diff --git a/apps/routerconsole/java/src/net/i2p/router/web/RouterConsoleRunner.java b/apps/routerconsole/java/src/net/i2p/router/web/RouterConsoleRunner.java
index e6734dd127d73d29f917110330390704c34f1086..46da0de60b33ad8e5cf90ba4f601dcccc24ab03f 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/RouterConsoleRunner.java
+++ b/apps/routerconsole/java/src/net/i2p/router/web/RouterConsoleRunner.java
@@ -833,7 +833,7 @@ public class RouterConsoleRunner implements RouterApp {
             }
         }
 
-        Thread t = new I2PAppThread(new StatSummarizer(), "StatSummarizer", true);
+        Thread t = new I2PAppThread(new StatSummarizer(_context), "StatSummarizer", true);
         t.setPriority(Thread.NORM_PRIORITY - 1);
         t.start();
         
diff --git a/apps/routerconsole/java/src/net/i2p/router/web/StatSummarizer.java b/apps/routerconsole/java/src/net/i2p/router/web/StatSummarizer.java
index 275d237f0d194ec7b97f4628d52aeb67ec774d66..e2b8067ddfd115ff905f5f9c6a120705aeaa64a6 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/StatSummarizer.java
+++ b/apps/routerconsole/java/src/net/i2p/router/web/StatSummarizer.java
@@ -11,6 +11,9 @@ import java.util.StringTokenizer;
 import java.util.concurrent.CopyOnWriteArrayList;
 import java.util.concurrent.Semaphore;
 
+import net.i2p.I2PAppContext;
+import net.i2p.app.ClientApp;
+import net.i2p.app.ClientAppState;
 import net.i2p.data.DataHelper;
 import net.i2p.router.RouterContext;
 import static net.i2p.router.web.GraphConstants.*;
@@ -32,29 +35,40 @@ import net.i2p.util.SystemVersion;
  *
  *  @since 0.6.1.13
  */
-public class StatSummarizer implements Runnable {
+public class StatSummarizer implements Runnable, ClientApp {
     private final RouterContext _context;
     private final Log _log;
     /** list of SummaryListener instances */
     private final List<SummaryListener> _listeners;
-    // TODO remove static instance
-    private static StatSummarizer _instance;
     private static final int MAX_CONCURRENT_PNG = SystemVersion.isARM() ? 2 : 3;
     private final Semaphore _sem;
-    private volatile boolean _isRunning = true;
-    private boolean _isDisabled;
-    private Thread _thread;
+    private volatile boolean _isRunning;
+    private volatile Thread _thread;
+    private static final String NAME = "StatSummarizer";
     
-    public StatSummarizer() {
-        _context = RouterContext.listContexts().get(0); // only summarize one per jvm
+    public StatSummarizer(RouterContext ctx) {
+        _context = ctx;
         _log = _context.logManager().getLog(getClass());
         _listeners = new CopyOnWriteArrayList<SummaryListener>();
-        _instance = this;
         _sem = new Semaphore(MAX_CONCURRENT_PNG, true);
         _context.addShutdownTask(new Shutdown());
     }
     
-    public static StatSummarizer instance() { return _instance; }
+    /**
+     * @return null if disabled
+     */
+    public static StatSummarizer instance() {
+        return instance(I2PAppContext.getGlobalContext());
+    }
+
+    /**
+     * @return null if disabled
+     * @since 0.0.38
+     */
+    public static StatSummarizer instance(I2PAppContext ctx) {
+        ClientApp app = ctx.clientAppManager().getRegisteredApp(NAME);
+        return (app != null) ? (StatSummarizer) app : null;
+    }
     
     public void run() {
         // JRobin 1.5.9 crashes these JVMs
@@ -65,24 +79,29 @@ public class StatSummarizer implements Runnable {
                                      System.getProperty("java.version") + " (" +
                                      System.getProperty("java.runtime.name") + ' ' +
                                      System.getProperty("java.runtime.version") + ')');
-            _isDisabled = true;
-            _isRunning = false;
             return;
         }
+        _isRunning = true;
         boolean isPersistent = _context.getBooleanPropertyDefaultTrue(SummaryListener.PROP_PERSISTENT);
         if (!isPersistent)
             deleteOldRRDs();
         _thread = Thread.currentThread();
+        _context.clientAppManager().register(this);
         String specs = "";
-        while (_isRunning && _context.router().isAlive()) {
-            specs = adjustDatabases(specs);
-            try { Thread.sleep(60*1000); } catch (InterruptedException ie) {}
+        try {
+            while (_isRunning && _context.router().isAlive()) {
+                specs = adjustDatabases(specs);
+                try { Thread.sleep(60*1000); } catch (InterruptedException ie) {}
+            }
+        } finally {
+            _isRunning = false;
+            _context.clientAppManager().unregister(this);
         }
     }
     
-    /** @since 0.8.7, public since 0.9.33, was package private */
-    public static boolean isDisabled() {
-        return _instance == null || _instance._isDisabled;
+    /** @since 0.0.38 */
+    public static boolean isDisabled(I2PAppContext ctx) {
+        return ctx.clientAppManager().getRegisteredApp(NAME) == null;
     }
     
     /**
@@ -90,13 +109,57 @@ public class StatSummarizer implements Runnable {
      * See SummaryRenderer.render()
      * @since 0.9.6
      */
-    static void setDisabled() {
-        if (_instance != null) {
-            _instance._isDisabled = true;
-            _instance._isRunning = false;
+    static void setDisabled(I2PAppContext ctx) {
+        StatSummarizer ss = instance(ctx);
+        if (ss != null)
+            ss.setDisabled();
+    }
+
+    /**
+     * Disable graph generation until restart
+     * See SummaryRenderer.render()
+     * @since 0.9.38
+     */
+    synchronized void setDisabled() {
+        if (_isRunning) {
+            _isRunning = false;
+            Thread t = _thread;
+            if (t != null)
+                t.interrupt();
         }
     }
 
+    /////// ClientApp methods
+
+    /**
+     * Does nothing, we aren't tracked
+     * @since 0.9.38
+     */
+    public void startup() {}
+
+    /**
+     * Does nothing, we aren't tracked
+     * @since 0.9.38
+     */
+    public void shutdown(String[] args) {}
+
+    /** @since 0.9.38 */
+    public ClientAppState getState() {
+        return ClientAppState.RUNNING;
+    }
+
+    /** @since 0.9.38 */
+    public String getName() {
+        return NAME;
+    }
+
+    /** @since 0.9.38 */
+    public String getDisplayName() {
+        return "Console stats summarizer";
+    }
+
+    /////// End ClientApp methods
+
     /**
      *  List of SummaryListener instances
      *  @since public since 0.9.33, was package private
@@ -206,8 +269,7 @@ public class StatSummarizer implements Runnable {
                 //  at java.lang.Class.forName0(Native Method)
                 //  at java.lang.Class.forName(Class.java:270)
                 //  at sun.font.FontManagerFactory$1.run(FontManagerFactory.java:82)
-                _isDisabled = true;
-                _isRunning = false;
+                setDisabled();
                 String s = "Error rendering - disabling graph generation. Install ttf-dejavu font package?";
                 _log.logAlways(Log.WARN, s);
                 IOException ioe = new IOException(s);
@@ -296,8 +358,7 @@ public class StatSummarizer implements Runnable {
                 //  at java.lang.Class.forName0(Native Method)
                 //  at java.lang.Class.forName(Class.java:270)
                 //  at sun.font.FontManagerFactory$1.run(FontManagerFactory.java:82)
-                _isDisabled = true;
-                _isRunning = false;
+                setDisabled();
                 String s = "Error rendering - disabling graph generation. Install ttf-dejavu font package?";
                 _log.logAlways(Log.WARN, s);
                 IOException ioe = new IOException(s);
@@ -316,7 +377,7 @@ public class StatSummarizer implements Runnable {
         // go to some trouble to see if we have the data for the combined bw graph
         SummaryListener txLsnr = null;
         SummaryListener rxLsnr = null;
-        for (SummaryListener lsnr : StatSummarizer.instance().getListeners()) {
+        for (SummaryListener lsnr : getListeners()) {
             String title = lsnr.getRate().getRateStat().getName();
             if (title.equals("bw.sendRate"))
                 txLsnr = lsnr;
@@ -396,9 +457,7 @@ public class StatSummarizer implements Runnable {
      */
     private class Shutdown implements Runnable {
         public void run() {
-            _isRunning = false;
-            if (_thread != null)
-                _thread.interrupt();
+            setDisabled();
             for (SummaryListener lsnr : _listeners) {
                 // FIXME could cause exceptions if rendering?
                 lsnr.stopListening();
diff --git a/apps/routerconsole/java/src/net/i2p/router/web/SummaryRenderer.java b/apps/routerconsole/java/src/net/i2p/router/web/SummaryRenderer.java
index 8571ef6ad64284abc3b027c335a931b85c8c1d52..ac66e9105d356a682a70f41890c90a5531a1a398 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/SummaryRenderer.java
+++ b/apps/routerconsole/java/src/net/i2p/router/web/SummaryRenderer.java
@@ -289,7 +289,7 @@ class SummaryRenderer {
                 graph = new RrdGraph(def);
             } catch (NullPointerException npe) {
                 _log.error("Error rendering", npe);
-                StatSummarizer.setDisabled();
+                StatSummarizer.setDisabled(_context);
                 throw new IOException("Error rendering - disabling graph generation. Missing font? See http://trac.i2p2.i2p/ticket/915");
             }
             int totalWidth = graph.getRrdGraphInfo().getWidth();
diff --git a/apps/routerconsole/java/src/net/i2p/router/web/helpers/GraphHelper.java b/apps/routerconsole/java/src/net/i2p/router/web/helpers/GraphHelper.java
index c7867a4f6d47624841c32992f2144113d0b05a65..848e8d113442caff7963e090f3f293e7c7bd3c1d 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/helpers/GraphHelper.java
+++ b/apps/routerconsole/java/src/net/i2p/router/web/helpers/GraphHelper.java
@@ -147,10 +147,11 @@ public class GraphHelper extends FormHandler {
     }
 
     public String getImages() { 
-        if (StatSummarizer.isDisabled())
+        StatSummarizer ss = StatSummarizer.instance(_context);
+        if (ss == null)
             return "";
         try {
-            List<SummaryListener> listeners = StatSummarizer.instance().getListeners();
+            List<SummaryListener> listeners = ss.getListeners();
             TreeSet<SummaryListener> ordered = new TreeSet<SummaryListener>(new AlphaComparator());
             ordered.addAll(listeners);
 
@@ -235,9 +236,10 @@ public class GraphHelper extends FormHandler {
      *  @since 0.9
      */
     public String getSingleStat() {
+        StatSummarizer ss = StatSummarizer.instance(_context);
+        if (ss == null)
+            return "";
         try {
-            if (StatSummarizer.isDisabled())
-                return "";
             if (_stat == null) {
                 _out.write("No stat specified");
                 return "";
@@ -249,7 +251,7 @@ public class GraphHelper extends FormHandler {
                 name = _stat;
                 displayName = _t("Bandwidth usage");
             } else {
-                Set<Rate> rates = StatSummarizer.instance().parseSpecs(_stat);
+                Set<Rate> rates = ss.parseSpecs(_stat);
                 if (rates.size() != 1) {
                     _out.write("Graphs not enabled for " + _stat);
                     return "";
@@ -376,7 +378,8 @@ public class GraphHelper extends FormHandler {
     private static final int[] times = { 15, 30, 60, 2*60, 5*60, 10*60, 30*60, 60*60, -1 };
 
     public String getForm() { 
-        if (StatSummarizer.isDisabled())
+        StatSummarizer ss = StatSummarizer.instance(_context);
+        if (ss == null)
             return "";
         // too hard to use the standard formhandler.jsi / FormHandler.java session nonces
         // since graphs.jsp needs the refresh value in its <head>.
@@ -440,7 +443,7 @@ public class GraphHelper extends FormHandler {
      */
     @Override
     public String getAllMessages() {
-        if (StatSummarizer.isDisabled()) {
+        if (StatSummarizer.isDisabled(_context)) {
             addFormError("Graphing not supported with this JVM: " +
                          System.getProperty("java.vendor") + ' ' +
                          System.getProperty("java.version") + " (" +
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 81e8956ebe254a5a931cee21227f1a7cd4c6a864..5f1a43caa23050d20f455c841a864866e85c12e3 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
@@ -277,7 +277,7 @@ class SummaryBarRenderer {
            .append("</a>\n");
         }
 
-        if (!StatSummarizer.isDisabled()) {
+        if (!StatSummarizer.isDisabled(_context)) {
             buf.append("<a href=\"/graphs\" target=\"_top\" title=\"")
                .append(_t("Graph router performance"))
                .append("\">")
@@ -822,13 +822,14 @@ class SummaryBarRenderer {
     public String renderBandwidthGraphHTML() {
         if (_helper == null) return "";
         StringBuilder buf = new StringBuilder(512);
-        if (!StatSummarizer.isDisabled())
+        if (!StatSummarizer.isDisabled(_context)) {
             buf.append("<div id=\"sb_graphcontainer\"><a href=\"/graphs\"><table id=\"sb_bandwidthgraph\">" +
                        "<tr title=\"")
                .append(_t("Our inbound &amp; outbound traffic for the last 20 minutes"))
                .append("\"><td><span id=\"sb_graphstats\">")
                .append(_helper.getSecondKBps())
                .append("Bps</span></td></tr></table></a></div>\n");
+        }
         buf.append("<script src=\"/js/refreshGraph.js\" type=\"text/javascript\" id=\"refreshGraph\" async></script>");
         return buf.toString();
     }
diff --git a/apps/routerconsole/jsp/viewstat.jsp b/apps/routerconsole/jsp/viewstat.jsp
index 892521b6a77b34c75fe89f786937d2a283259d90..f388d39c0aab3532b1ef88334d1736b1fd830979 100644
--- a/apps/routerconsole/jsp/viewstat.jsp
+++ b/apps/routerconsole/jsp/viewstat.jsp
@@ -6,14 +6,19 @@
  *
  * Do not tag this file for translation.
  */
-
+net.i2p.I2PAppContext ctx = net.i2p.I2PAppContext.getGlobalContext();
+net.i2p.router.web.StatSummarizer ss = net.i2p.router.web.StatSummarizer.instance(ctx);
+if (ss == null) {
+    response.sendError(403, "Stats disabled");
+    return;
+}
 boolean rendered = false;
 /****  unused
 String templateFile = request.getParameter("template");
 if (templateFile != null) {
   java.io.OutputStream cout = response.getOutputStream();
   response.setContentType("image/png");
-  rendered = net.i2p.router.web.StatSummarizer.instance().renderPng(cout, templateFile);
+  rendered = ss.renderPng(cout, templateFile);
 }
 ****/
 net.i2p.stat.Rate rate = null;
@@ -22,7 +27,7 @@ String period = request.getParameter("period");
 boolean fakeBw = (stat != null && ("bw.combined".equals(stat)));
 net.i2p.stat.RateStat rs = null;
 if (stat != null)
-    rs = net.i2p.I2PAppContext.getGlobalContext().statManager().getRate(stat);
+    rs = ctx.statManager().getRate(stat);
 if ( !rendered && ((rs != null) || fakeBw) ) {
   long per = -1;
   try {
@@ -39,12 +44,12 @@ if ( !rendered && ((rs != null) || fakeBw) ) {
       if ("xml".equals(format)) {
         if (!fakeBw) {
           response.setContentType("text/xml");
-          rendered = net.i2p.router.web.StatSummarizer.instance().getXML(rate, cout);
+          rendered = ss.getXML(rate, cout);
         }
       } else {
         response.setContentType("image/png");
         // very brief 45 sec expire
-        response.setDateHeader("Expires", net.i2p.I2PAppContext.getGlobalContext().clock().now() + (45*1000));
+        response.setDateHeader("Expires", ctx.clock().now() + (45*1000));
         response.setHeader("Accept-Ranges", "none");
         // http://jira.codehaus.org/browse/JETTY-1346
         // This doesn't actually appear in the response, but it fixes the problem,
@@ -70,9 +75,9 @@ if ( !rendered && ((rs != null) || fakeBw) ) {
         if (request.getParameter("showCredit") != null)
           showCredit = Boolean.parseBoolean(request.getParameter("showCredit"));
         if (fakeBw)
-            rendered = net.i2p.router.web.StatSummarizer.instance().renderRatePng(cout, width, height, hideLegend, hideGrid, hideTitle, showEvents, periodCount, end, showCredit);
+            rendered = ss.renderRatePng(cout, width, height, hideLegend, hideGrid, hideTitle, showEvents, periodCount, end, showCredit);
         else
-            rendered = net.i2p.router.web.StatSummarizer.instance().renderPng(rate, cout, width, height, hideLegend, hideGrid, hideTitle, showEvents, periodCount, end, showCredit);
+            rendered = ss.renderPng(rate, cout, width, height, hideLegend, hideGrid, hideTitle, showEvents, periodCount, end, showCredit);
       }
       if (rendered)
         cout.close();