diff --git a/apps/addressbook/java/src/net/i2p/addressbook/Daemon.java b/apps/addressbook/java/src/net/i2p/addressbook/Daemon.java
index 2eaa54881196d6aa13615ed613bc1bd8679f41b4..60597a5f9d4f494e1fa8eedaedac0bc729fd3cba 100644
--- a/apps/addressbook/java/src/net/i2p/addressbook/Daemon.java
+++ b/apps/addressbook/java/src/net/i2p/addressbook/Daemon.java
@@ -32,12 +32,14 @@ import java.util.Properties;
 import java.util.Set;
 
 import net.i2p.I2PAppContext;
+import net.i2p.app.ClientAppManager;
 import net.i2p.client.naming.HostTxtEntry;
 import net.i2p.client.naming.NamingService;
 import net.i2p.client.naming.SingleFileNamingService;
 import net.i2p.data.DataFormatException;
 import net.i2p.data.Destination;
 import net.i2p.util.OrderedProperties;
+import net.i2p.util.PortMapper;
 import net.i2p.util.SecureDirectory;
 import net.i2p.util.SystemVersion;
 
@@ -669,6 +671,13 @@ class Daemon {
                            invalid + " invalid, " +
                            conflict + " conflicts");
             }
+            if (nnew > 0) {
+                ClientAppManager cmgr = I2PAppContext.getGlobalContext().clientAppManager();
+                if (cmgr != null) {
+                    int nc = cmgr.getBubbleCount(PortMapper.SVC_SUSIDNS);
+                    cmgr.setBubble(PortMapper.SVC_SUSIDNS, nc + nnew, null);
+                }
+            }
     }
 
     /** @since 0.9.26 */
diff --git a/apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java b/apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java
index 0027f8ed080f0c9e10d85a4003a7ff5159898b9f..d34c3426c37a9b61933804ecbe8b832225f957eb 100644
--- a/apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java
+++ b/apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java
@@ -45,6 +45,7 @@ import net.i2p.util.FileUtil;
 import net.i2p.util.I2PAppThread;
 import net.i2p.util.Log;
 import net.i2p.util.OrderedProperties;
+import net.i2p.util.PortMapper;
 import net.i2p.util.SecureDirectory;
 import net.i2p.util.SecureFileOutputStream;
 import net.i2p.util.SimpleTimer;
@@ -501,7 +502,10 @@ public class SnarkManager implements CompleteListener, ClientApp, DisconnectList
     
     /** @since 0.9 */
     public void clearMessages() {
-            _messages.clear();
+        _messages.clear();
+        ClientAppManager cmgr = _context.clientAppManager();
+        if (cmgr != null)
+            cmgr.setBubble(PortMapper.SVC_I2PSNARK, 0, null);
     }
     
     /**
@@ -509,7 +513,10 @@ public class SnarkManager implements CompleteListener, ClientApp, DisconnectList
      *  @since 0.9.33
      */
     public void clearMessages(int id) {
-            _messages.clearThrough(id);
+        _messages.clearThrough(id);
+        ClientAppManager cmgr = _context.clientAppManager();
+        if (cmgr != null)
+            cmgr.setBubble(PortMapper.SVC_I2PSNARK, 0, null);
     }
     
     /**
@@ -2876,6 +2883,7 @@ public class SnarkManager implements CompleteListener, ClientApp, DisconnectList
             NotificationService ns = (NotificationService) cmgr.getRegisteredApp("desktopgui");
             if (ns != null)
                 ns.notify("I2PSnark", null, priority, _t("I2PSnark"), message, path);
+            cmgr.addBubble(PortMapper.SVC_I2PSNARK, message);
         }
         if (!_context.isRouterContext())
             System.out.println(message);
diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelServer.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelServer.java
index b9e012d7e8513bb6358f3686a8d13f6360a14df4..0f9d69c5caa0bc23f7e35afe46d9ab45935994da 100644
--- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelServer.java
+++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelServer.java
@@ -297,6 +297,7 @@ public class I2PTunnelServer extends I2PTunnelTask implements Runnable {
                     else
                         msg = "Offline signature for tunnel " + name + " alternate destination expired " + DataHelper.formatTime(exp);
                     _log.log(Log.CRIT, msg);
+                    TunnelController.addBubble(getTunnel().getContext(), msg);
                     throw new IllegalArgumentException(msg);
                 }
                 if (remaining < 60*24*60*60*1000L) {
@@ -346,6 +347,7 @@ public class I2PTunnelServer extends I2PTunnelTask implements Runnable {
                 else
                     msg = "Offline signature for tunnel " + name + " expired " + DataHelper.formatTime(exp);
                 _log.log(Log.CRIT, msg);
+                TunnelController.addBubble(getTunnel().getContext(), msg);
                 throw new IllegalArgumentException(msg);
             }
             if (remaining < 60*24*60*60*1000L) {
@@ -394,6 +396,7 @@ public class I2PTunnelServer extends I2PTunnelTask implements Runnable {
                     msg += ", giving up";
                     this.l.log(msg);
                     _log.log(Log.CRIT, msg, ise);
+                    TunnelController.addBubble(getTunnel().getContext(), msg);
                     throw new IllegalArgumentException(msg, ise);
                 }
                 try { Thread.sleep(RETRY_DELAY); } catch (InterruptedException ie) {}
@@ -686,6 +689,7 @@ public class I2PTunnelServer extends I2PTunnelTask implements Runnable {
                 String s = "Error accepting - KILLING THE TUNNEL SERVER";
                 _log.log(Log.CRIT, s, ipe);
                 l.log(s + ": " + ipe);
+                TunnelController.addBubble(getTunnel().getContext(), s);
                 // Tell TunnelController so it will change state
                 TunnelController tc = getTunnel().getController();
                 if (tc != null)
diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/TunnelController.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/TunnelController.java
index 0b9855368dc82102fcb193c645bfed850bd5b78a..75b1630c2fa2db4e8df56750603a93aea687cd20 100644
--- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/TunnelController.java
+++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/TunnelController.java
@@ -14,6 +14,7 @@ import java.util.Set;
 
 import net.i2p.I2PAppContext;
 import net.i2p.I2PException;
+import net.i2p.app.ClientAppManager;
 import net.i2p.client.I2PClient;
 import net.i2p.client.I2PClientFactory;
 import net.i2p.client.I2PSession;
@@ -34,6 +35,7 @@ import net.i2p.i2ptunnel.socks.I2PSOCKSTunnel;
 import net.i2p.util.FileUtil;
 import net.i2p.util.I2PAppThread;
 import net.i2p.util.Log;
+import net.i2p.util.PortMapper;
 import net.i2p.util.RandomSource;
 import net.i2p.util.SecureFile;
 import net.i2p.util.SecureFileOutputStream;
@@ -429,8 +431,10 @@ public class TunnelController implements Logging {
         try {
             doStartTunnel();
         } catch (RuntimeException e) {
-            _log.error("Error starting the tunnel " + getName(), e);
-            log("Error starting the tunnel " + getName() + ": " + e.getMessage());
+            String msg = "Error starting the tunnel " + getName();
+            _log.error(msg, e);
+            addBubble(msg);
+            log(msg + ": " + e.getMessage());
             // if we don't acquire() then the release() in stopTunnel() won't work
             acquire();
             stopTunnel();
@@ -1398,6 +1402,25 @@ public class TunnelController implements Logging {
         return rv;
     }
 
+    /**
+     * @param msg may be null
+     * @since 0.9.66
+     */
+    private void addBubble(String msg) {
+        addBubble(_tunnel.getContext(), msg);
+    }
+
+    /**
+     * @param msg may be null
+     * @since 0.9.66
+     */
+    static void addBubble(I2PAppContext ctx, String msg) {
+        ClientAppManager cmgr = ctx.clientAppManager();
+        if (cmgr != null) {
+            cmgr.addBubble(PortMapper.SVC_I2PTUNNEL, msg);
+        }
+    }
+
     /**
      * @since 0.9.15
      */
@@ -1477,6 +1500,7 @@ public class TunnelController implements Logging {
                             msg = "Offline signature in private key file " + f + " for tunnel expired " + DataHelper.formatTime(exp) + ", stopping the tunnel!";
                         _log.log(Log.CRIT, msg);
                         _tunnel.log(msg);
+                        addBubble(msg);
                         stopTunnel();
                         return;
                     }
diff --git a/apps/routerconsole/java/src/net/i2p/router/web/helpers/HomeHelper.java b/apps/routerconsole/java/src/net/i2p/router/web/helpers/HomeHelper.java
index 34f09b7d6e0c2675b970e05dff61b03e3adda1a4..49499ab0c9579abb7b254ebbc884e6b903293a56 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/helpers/HomeHelper.java
+++ b/apps/routerconsole/java/src/net/i2p/router/web/helpers/HomeHelper.java
@@ -8,6 +8,7 @@ import java.util.Set;
 import java.util.TreeSet;
 
 import net.i2p.CoreVersion;
+import net.i2p.app.ClientAppManager;
 import net.i2p.data.DataHelper;
 import net.i2p.router.RouterContext;
 import net.i2p.router.web.App;
@@ -388,7 +389,9 @@ Steps for the devs after approval at a meeting:
         StringBuilder buf = new StringBuilder(1024);
         buf.append("<div class=\"appgroup\">");
         PortMapper pm = _context.portMapper();
+        ClientAppManager cmgr = _context.clientAppManager();
         for (App app : apps) {
+            String svc = null;
             String url;
             if (app.name.equals(website) && app.url.equals("http://127.0.0.1:7658/")) {
                 // fixup I2P Site link
@@ -399,27 +402,27 @@ Steps for the devs after approval at a meeting:
                 url = app.url;
                 // check for disabled webapps and other things
                 if (url.equals("/dns")) {
-                    if (!pm.isRegistered(PortMapper.SVC_SUSIDNS))
-                        continue;
+                    svc = PortMapper.SVC_SUSIDNS;
                 } else if (url.equals("/webmail")) {
-                    if (!pm.isRegistered(PortMapper.SVC_SUSIMAIL))
-                        continue;
+                    svc = PortMapper.SVC_SUSIMAIL;
                 } else if (url.equals("/torrents")) {
-                    if (!pm.isRegistered(PortMapper.SVC_I2PSNARK))
-                        continue;
+                    svc = PortMapper.SVC_I2PSNARK;
                 } else if (url.equals("/i2ptunnelmgr")) {
-                    if (!pm.isRegistered(PortMapper.SVC_I2PTUNNEL))
-                        continue;
+                    svc = PortMapper.SVC_I2PTUNNEL;
                     // need both webapp and TCG, but we aren't refreshing
                     // the icons, so let's not do this
                     //ClientAppManager cmgr = _context.clientAppManager();
                     //if (cmgr != null && cmgr.getRegisteredApp("i2ptunnel") == null)
                     //    continue;
+                } else if (url.equals("/logs")) {
+                    svc = PortMapper.SVC_LOGS;
                 } else if (url.equals("/configplugins")) {
                     if (!PluginStarter.pluginsEnabled(_context))
                         continue;
                 }
             }
+            if (svc != null && !pm.isRegistered(svc))
+                continue;
             // If an image isn't in a /themes or /images directory, it comes from a plugin.
             // tag it thus.
             String plugin = "";
@@ -432,8 +435,21 @@ Steps for the devs after approval at a meeting:
                        "<a href=\"").append(url).append("\" tabindex=\"-1\">" +
                        "<img alt=\"\" title=\"").append(app.desc).append("\" src=\"").append(app.icon)
                // version the icons because they may change
-               .append(app.icon.contains("?") ? "&amp;" : "?").append(CoreVersion.VERSION).append("\"></a>" +
-                       "</div>\n" +
+               .append(app.icon.contains("?") ? "&amp;" : "?").append(CoreVersion.VERSION).append("\">");
+            // notification bubbles
+            if (svc != null && cmgr != null) {
+                int nc = cmgr.getBubbleCount(svc);
+                if (nc > 0) {
+                    buf.append("<span class=\"notifbubble\" ");
+                    String ns = cmgr.getBubbleText(svc);
+                    if (ns != null)
+                        buf.append(" title=\"").append(DataHelper.escapeHTML(ns)).append("\" ");
+                    buf.append('>');
+                    buf.append(nc);
+                    buf.append("</span>");
+                }
+            }
+            buf.append("</a></div>\n" +
                        "<table><tr><td>" +
                        "<div class=\"applabel\">" +
                        "<a href=\"").append(url).append("\" title=\"").append(app.desc).append("\">").append(app.name).append("</a>" +
diff --git a/apps/routerconsole/java/src/net/i2p/router/web/helpers/LogsHelper.java b/apps/routerconsole/java/src/net/i2p/router/web/helpers/LogsHelper.java
index c86168841d5e70b0bc92e8cac33a60ceced88a9b..f63e3c8cee8c7a61eb5223a2946c2c8b5ff9aca8 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/helpers/LogsHelper.java
+++ b/apps/routerconsole/java/src/net/i2p/router/web/helpers/LogsHelper.java
@@ -12,12 +12,14 @@ import java.util.concurrent.ArrayBlockingQueue;
 import java.util.jar.Attributes;
 
 import net.i2p.I2PAppContext;
+import net.i2p.app.ClientAppManager;
 import net.i2p.crypto.SigType;
 import net.i2p.data.DataHelper;
 import net.i2p.router.web.ConfigServiceHandler;
 import net.i2p.router.web.CSSHelper;
 import net.i2p.router.web.HelperBase;
 import net.i2p.router.web.RouterConsoleRunner;
+import net.i2p.util.PortMapper;
 import net.i2p.util.Translate;
 import net.i2p.util.UIMessages;
 
@@ -88,7 +90,13 @@ public class LogsHelper extends HelperBase {
      *
      */
     public String getCriticalLogs() {
-        return formatMessages(_context.logManager().getBuffer().getMostRecentCriticalMessages());
+        List<String> msgs = _context.logManager().getBuffer().getMostRecentCriticalMessages();
+        if (!msgs.isEmpty()) {
+            ClientAppManager cmgr = _context.clientAppManager();
+            if (cmgr != null)
+                cmgr.setBubble(PortMapper.SVC_LOGS, 0, null);
+        }
+        return formatMessages(msgs);
     }
 
     /**
diff --git a/apps/routerconsole/java/src/net/i2p/router/web/helpers/SummaryBarRenderer.java b/apps/routerconsole/java/src/net/i2p/router/web/helpers/SummaryBarRenderer.java
index 523c77ecd08dcc702f059696a08dd7e0b717db66..9d2da1f2dbf8f264c1e9614a81f402da55f23dfb 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/helpers/SummaryBarRenderer.java
+++ b/apps/routerconsole/java/src/net/i2p/router/web/helpers/SummaryBarRenderer.java
@@ -226,8 +226,9 @@ class SummaryBarRenderer {
                         "<a href=\"/webmail\" target=\"_top\" title=\"")
                 .append(_t("Anonymous webmail client"))
                 .append("\">")
-                .append(nbsp(tx))
-                .append("</a></td></tr>\n");
+                .append(nbsp(tx));
+            addBubble(rbuf, PortMapper.SVC_SUSIMAIL);
+            rbuf.append("</a></td></tr>\n");
             svcs.put(tx, rbuf.toString());
         }
 
@@ -250,8 +251,9 @@ class SummaryBarRenderer {
                         "<a href=\"/torrents\" target=\"_top\" title=\"")
                 .append(_t("Built-in anonymous BitTorrent Client"))
                 .append("\">")
-                .append(nbsp(tx))
-                .append("</a></td></tr>\n");
+                .append(nbsp(tx));
+            addBubble(rbuf, PortMapper.SVC_I2PSNARK);
+            rbuf.append("</a></td></tr>\n");
             svcs.put(tx, rbuf.toString());
         }
 
@@ -299,6 +301,25 @@ class SummaryBarRenderer {
         }
     }
 
+    /**
+     *  @since 0.9.66
+     */
+    private void addBubble(StringBuilder buf, String svc) {
+        ClientAppManager cmgr = _context.clientAppManager();
+        if (cmgr == null)
+            return;
+        int nc = cmgr.getBubbleCount(svc);
+        if (nc <= 0)
+            return;
+        buf.append(" <span class=\"notifcount\" ");
+        String ns = cmgr.getBubbleText(svc);
+        if (ns != null)
+            buf.append(" title=\"").append(DataHelper.escapeHTML(ns)).append("\" ");
+        buf.append('>');
+        buf.append(nc);
+        buf.append("</span>");
+    }
+
     /**
      *  @return null if none
      *  @since 0.9.43 split out from above, used by HomeHelper, fixed for IPv6
@@ -349,8 +370,9 @@ class SummaryBarRenderer {
             rbuf.append("<a href=\"/dns\" target=\"_top\" title=\"")
                 .append(_t("Manage your I2P hosts file here (I2P domain name resolution)"))
                 .append("\">")
-                .append(nbsp(tx))
-                .append("</a>\n");
+                .append(nbsp(tx));
+            addBubble(rbuf, PortMapper.SVC_SUSIDNS);
+            rbuf.append("</a>\n");
             svcs.put(tx, rbuf.toString());
         }
 
@@ -360,8 +382,9 @@ class SummaryBarRenderer {
             rbuf.append("<a href=\"/i2ptunnelmgr\" target=\"_top\" title=\"")
                 .append(_t("Local Tunnels"))
                 .append("\">")
-                .append(nbsp(tx))
-                .append("</a>\n");
+                .append(nbsp(tx));
+            addBubble(rbuf, PortMapper.SVC_I2PTUNNEL);
+            rbuf.append("</a>\n");
             svcs.put(tx, rbuf.toString());
         }
 
@@ -434,8 +457,9 @@ class SummaryBarRenderer {
         rbuf.append("<a href=\"/logs\" target=\"_top\" title=\"")
             .append(_t("Health Report"))
             .append("\">")
-            .append(nbsp(tx))
-            .append("</a>\n");
+            .append(nbsp(tx));
+        addBubble(rbuf, PortMapper.SVC_LOGS);
+        rbuf.append("</a>\n");
         svcs.put(tx, rbuf.toString());
 
         tx = _t("NetDB");
diff --git a/apps/routerconsole/jsp/themes/console/dark/console.css b/apps/routerconsole/jsp/themes/console/dark/console.css
index 4e3da837032ecfb9a3c496888027a36e659e4aac..07e777c36f56d8ee0309b04f5799a4f4a0bd9dba 100644
--- a/apps/routerconsole/jsp/themes/console/dark/console.css
+++ b/apps/routerconsole/jsp/themes/console/dark/console.css
@@ -635,6 +635,25 @@ p:empty+.sb_notice {
     margin-top: 9px;
 }
 
+.notifcount {
+    background-color: #e33;
+    border: 1px solid #e33;
+    border-radius: 9px;
+    color: #fff;
+    margin: 0 6px 0 3px;
+    padding: 0 4px;
+}
+
+.notifbubble {
+    background-color: #e33;
+    border: 1px solid #e33;
+    border-radius: 9px;
+    color: #fff;
+    margin: -6px 0 0 -70px;
+    padding: 0 4px;
+    position: absolute;
+}
+
 .routersummary tr {
     background-image: none !important;
     background-color: transparent !important;
diff --git a/apps/routerconsole/jsp/themes/console/light/console.css b/apps/routerconsole/jsp/themes/console/light/console.css
index 523280ecbb541ab1632742ac93b8ce74c6840232..a4b87e5fbb3a24a25db1ee677dce42d2e7db45ce 100644
--- a/apps/routerconsole/jsp/themes/console/light/console.css
+++ b/apps/routerconsole/jsp/themes/console/light/console.css
@@ -779,6 +779,24 @@ p:empty+.sb_notice {
     background-color: #e9ecef;
 }
 
+.notifcount {
+    background-color: #e33;
+    border: 1px solid #e33;
+    border-radius: 9px;
+    color: #fff;
+    margin: 0 6px 0 3px;
+    padding: 0 4px;
+}
+
+.notifbubble {
+    background-color: #e33;
+    border: 1px solid #e33;
+    border-radius: 9px;
+    color: #fff;
+    margin: -6px 0 0 -70px;
+    padding: 0 4px;
+    position: absolute;
+}
 
 /* end webapp navigation */
 
diff --git a/apps/susimail/src/src/i2p/susi/webmail/MailCache.java b/apps/susimail/src/src/i2p/susi/webmail/MailCache.java
index 5bcf9c877a3dd0a405a89d8ebd007f561f9eed05..9f288a4750ab11ecb6ba07699c538e2700eb2d43 100644
--- a/apps/susimail/src/src/i2p/susi/webmail/MailCache.java
+++ b/apps/susimail/src/src/i2p/susi/webmail/MailCache.java
@@ -52,6 +52,7 @@ import net.i2p.data.Base64;
 import net.i2p.util.FileUtil;
 import net.i2p.util.I2PAppThread;
 import net.i2p.util.Log;
+import net.i2p.util.PortMapper;
 
 /**
  * There's one of these for each Folder.
@@ -540,16 +541,19 @@ class MailCache {
 				}
 			}
 			if (newMail > 0) {
-				// DTG popup
+				String msg = ngettext("{0} new message", "{0} new messages", newMail);
+				if (newMail == 1 && additionalMsg != null)
+					msg += additionalMsg;
 				ClientAppManager cmgr = _context.clientAppManager();
 				if (cmgr != null) {
 					NotificationService ns = (NotificationService) cmgr.getRegisteredApp("desktopgui");
 					if (ns != null) {
-						String msg = ngettext("{0} new message", "{0} new messages", newMail);
-						if (newMail == 1 && additionalMsg != null)
-							msg += additionalMsg;
+						// DTG popup
 						ns.notify("SusiMail", null, Log.INFO, _t("Email"), msg, "/susimail/");
 					}
+					// Console sidebar
+					int nc = cmgr.getBubbleCount(PortMapper.SVC_SUSIMAIL);
+					cmgr.setBubble(PortMapper.SVC_SUSIMAIL, nc + newMail, msg);
 				}
 			}
 		}
diff --git a/apps/susimail/src/src/i2p/susi/webmail/WebMail.java b/apps/susimail/src/src/i2p/susi/webmail/WebMail.java
index 356d1fc31a8cbdadb7bcae47a9ffe4d0a5f817f3..bb0951bfd708b5a3cc7b3221d04ba53b4c1acf2d 100644
--- a/apps/susimail/src/src/i2p/susi/webmail/WebMail.java
+++ b/apps/susimail/src/src/i2p/susi/webmail/WebMail.java
@@ -89,6 +89,7 @@ import javax.servlet.http.HttpSessionBindingListener;
 
 import net.i2p.CoreVersion;
 import net.i2p.I2PAppContext;
+import net.i2p.app.ClientAppManager;
 import net.i2p.data.Base64;
 import net.i2p.data.DataHelper;
 import net.i2p.servlet.RequestWrapper;
@@ -96,6 +97,7 @@ import net.i2p.servlet.util.ServletUtil;
 import net.i2p.servlet.util.WriterOutputStream;
 import net.i2p.util.I2PAppThread;
 import net.i2p.util.Log;
+import net.i2p.util.PortMapper;
 import net.i2p.util.RFC822Date;
 import net.i2p.util.SecureFileOutputStream;
 import net.i2p.util.Translate;
@@ -4122,7 +4124,21 @@ public class WebMail extends HttpServlet
 					"<tr><td colspan=\"2\" align=\"center\"><hr></td></tr>" +
 					"</table></td></tr>\n" );
 			if( mail.hasPart()) {
-				mail.setNew(false);
+				if (mail.isNew()) {
+					mail.setNew(false);
+					I2PAppContext ctx = I2PAppContext.getGlobalContext();
+					ClientAppManager cmgr = ctx.clientAppManager();
+					if (cmgr != null) {
+						int nc = cmgr.getBubbleCount(PortMapper.SVC_SUSIMAIL);
+						if (nc > 0) {
+							nc--;
+							String msg = ngettext("{0} new message", "{0} new messages", nc);
+							cmgr.setBubble(PortMapper.SVC_SUSIMAIL, --nc, msg);
+						} else {
+							cmgr.setBubble(PortMapper.SVC_SUSIMAIL, 0, null);
+						}
+					}
+				}
 				showPart(out, mail.getPart(), 0, SHOW_HTML, allowHTML);
 			}
 			else {
diff --git a/core/java/src/net/i2p/app/ClientAppManager.java b/core/java/src/net/i2p/app/ClientAppManager.java
index 0830b7390cc16aed6f896b4191da6a1d04cad46a..626842e8b0105f5dc855705d15f81236e9508e51 100644
--- a/core/java/src/net/i2p/app/ClientAppManager.java
+++ b/core/java/src/net/i2p/app/ClientAppManager.java
@@ -45,4 +45,32 @@ public interface ClientAppManager {
      *  @return client app or null
      */
     public ClientApp getRegisteredApp(String name);
+
+    /**
+     *  Bubble count
+     *  @since 0.9.66
+     */
+    public int getBubbleCount(String svc);
+
+    /**
+     *  Bubble message, translated, not HTML escaped
+     *  @return null if none
+     *  @since 0.9.66
+     */
+    public String getBubbleText(String svc);
+
+    /**
+     *  Update notifications for service
+     *  @param count 0 to clear
+     *  @param text translated, not HTML escaped, null if none
+     *  @since 0.9.66
+     */
+    public void setBubble(String svc, int count, String text);
+
+    /**
+     *  Increment the count and set the text
+     *  @param text translated, not HTML escaped, null if none
+     *  @since 0.9.66
+     */
+    public void addBubble(String svc, String text);
 }
diff --git a/core/java/src/net/i2p/app/ClientAppManagerImpl.java b/core/java/src/net/i2p/app/ClientAppManagerImpl.java
index 1c7342c42aea65b2f91f2e92779bc24ec56b6b6e..565896393866a65b557e121bb68fc6c7ac4b48df 100644
--- a/core/java/src/net/i2p/app/ClientAppManagerImpl.java
+++ b/core/java/src/net/i2p/app/ClientAppManagerImpl.java
@@ -64,4 +64,32 @@ public class ClientAppManagerImpl implements ClientAppManager {
     public ClientApp getRegisteredApp(String name) {
         return _registered.get(name);
     }
+
+    /**
+     *  @return 0 always, see RouterAppManager override
+     *  @since 0.9.66
+     */
+    public int getBubbleCount(String svc) {
+        return 0;
+    }
+
+    /**
+     *  @return null always, see RouterAppManager override
+     *  @since 0.9.66
+     */
+    public String getBubbleText(String svc) {
+        return null;
+    }
+
+    /**
+     *  Does nothing, see RouterAppManager override
+     *  @since 0.9.66
+     */
+    public void setBubble(String svc, int count, String text) {}
+
+    /**
+     *  Does nothing, see RouterAppManager override
+     *  @since 0.9.66
+     */
+    public synchronized void addBubble(String svc, String text) {}
 }
diff --git a/core/java/src/net/i2p/util/LogManager.java b/core/java/src/net/i2p/util/LogManager.java
index 76493b65ba1766234ff36b778bd5527762824f6e..5eb0db888c04617e9200a8052042aa71ea9f6003 100644
--- a/core/java/src/net/i2p/util/LogManager.java
+++ b/core/java/src/net/i2p/util/LogManager.java
@@ -166,6 +166,9 @@ public class LogManager implements Flushable {
         if (context.isRouterContext()) {
             // FIXME don't start thread in constructor
             startLogWriter();
+            // for bubbles, host/port may be wrong
+            PortMapper pm = context.portMapper();
+            pm.register(PortMapper.SVC_LOGS, "127.0.0.1", 7657);
         } else {
             // Only in App Context.
             // In Router Context, the router has its own shutdown hook,
diff --git a/core/java/src/net/i2p/util/LogWriter.java b/core/java/src/net/i2p/util/LogWriter.java
index 450c51a34591696e52e1d1902a4816d5e485defd..d103d4e887369ed6f3e36cd61d98060c1f64d9bd 100644
--- a/core/java/src/net/i2p/util/LogWriter.java
+++ b/core/java/src/net/i2p/util/LogWriter.java
@@ -228,6 +228,10 @@ abstract class LogWriter implements Runnable {
                             String tname = Translate.getString(name, _manager.getContext(), ROUTER_BUNDLE_NAME);
                             ns.notify(name, null, priority, tname, msg, null);
                         }
+                        if (priority >= Log.CRIT) {
+                            // Console sidebar
+                            cmgr.addBubble(PortMapper.SVC_LOGS, msg);
+                        }
                     }
                 }
             }
diff --git a/core/java/src/net/i2p/util/PortMapper.java b/core/java/src/net/i2p/util/PortMapper.java
index 3553c9b5ae166ab0c384d25210438541ca990fb7..0694aa0ff72924daf22b3331330257b1f32c433f 100644
--- a/core/java/src/net/i2p/util/PortMapper.java
+++ b/core/java/src/net/i2p/util/PortMapper.java
@@ -91,6 +91,11 @@ public class PortMapper {
      *  @since 0.9.39
      */
     public static final String SVC_JSONRPC = "jsonrpc";
+    /**
+     *  For bubbles
+     *  @since 0.9.66
+     */
+    public static final String SVC_LOGS = "logs";
 
     /** @since 0.9.34 */
     public static final int DEFAULT_CONSOLE_PORT = 7657;
diff --git a/router/java/src/net/i2p/router/startup/RouterAppManager.java b/router/java/src/net/i2p/router/startup/RouterAppManager.java
index ec3636f796ae5710afaae32e7e18c38b23c75b79..561a9f1ad1ed50145bbfe0d5b31d977b79c9de0e 100644
--- a/router/java/src/net/i2p/router/startup/RouterAppManager.java
+++ b/router/java/src/net/i2p/router/startup/RouterAppManager.java
@@ -16,6 +16,7 @@ import net.i2p.app.*;
 import static net.i2p.app.ClientAppState.*;
 import net.i2p.router.RouterContext;
 import net.i2p.util.Log;
+import net.i2p.util.SystemVersion;
 
 /**
  *  Notify the router of events, and provide methods for
@@ -30,12 +31,14 @@ public class RouterAppManager extends ClientAppManagerImpl {
     // client to args
     // this assumes clients do not override equals()
     private final ConcurrentHashMap<ClientApp, String[]> _clients;
+    private final ConcurrentHashMap<String, Bubble> _bubbles;
 
     public RouterAppManager(RouterContext ctx) {
         super(ctx);
         _context = ctx;
         _log = ctx.logManager().getLog(RouterAppManager.class);
         _clients = new ConcurrentHashMap<ClientApp, String[]>(16);
+        _bubbles = new ConcurrentHashMap<String, Bubble>(4);
         ctx.addShutdownTask(new Shutdown());
     }
 
@@ -192,6 +195,74 @@ public class RouterAppManager extends ClientAppManagerImpl {
                 _log.info("Client " + app.getDisplayName() + " UNREGISTERED AS " + app.getName());
         }
         super.unregister(app);
+        _bubbles.remove(app.getName());
+    }
+
+    /**
+     *  Bubbles
+     *  @since 0.9.66
+     */
+    private static class Bubble {
+        public final int cnt;
+        public final String text;
+
+        public Bubble(int count, String msg) {
+            cnt = count;
+            text = msg;
+        }
+    }
+
+    /**
+     *  Bubble count
+     *  @since 0.9.66
+     */
+    @Override
+    public int getBubbleCount(String svc) {
+        Bubble n = _bubbles.get(svc);
+        if (n == null)
+            return 0;
+        return n.cnt;
+    }
+
+    /**
+     *  Bubble message, translated, not HTML escaped
+     *  @return null if none
+     *  @since 0.9.66
+     */
+    @Override
+    public String getBubbleText(String svc) {
+        Bubble n = _bubbles.get(svc);
+        if (n == null)
+            return null;
+        return n.text;
+    }
+
+    /**
+     *  Update notifications for service
+     *  @param count 0 to clear
+     *  @param text translated, not HTML escaped, null if none
+     *  @since 0.9.66
+     */
+    @Override
+    public void setBubble(String svc, int count, String text) {
+        if (SystemVersion.isAndroid())
+            return;
+        if (count == 0 && text == null) {
+            _bubbles.remove(svc);
+        } else {
+            Bubble n = new Bubble(count, text);
+            _bubbles.put(svc, n);
+        }
+    }
+
+    /**
+     *  Increment the count and set the text
+     *  @param text translated, not HTML escaped, null if none
+     *  @since 0.9.66
+     */
+    @Override
+    public synchronized void addBubble(String svc, String text) {
+        setBubble(svc, getBubbleCount(svc) + 1, text);
     }
 
     /// end ClientAppManager interface