diff --git a/apps/routerconsole/java/src/net/i2p/router/web/ConfigClientsHandler.java b/apps/routerconsole/java/src/net/i2p/router/web/ConfigClientsHandler.java
index 5a9d991c93d73cac0abb926107b622a16d2371dd..d6f46ae8e7293e0daa87fb43b6bccfd6a421927d 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/ConfigClientsHandler.java
+++ b/apps/routerconsole/java/src/net/i2p/router/web/ConfigClientsHandler.java
@@ -1,59 +1,115 @@
 package net.i2p.router.web;
 
+import java.util.Collection;
 import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
 import java.util.Map;
+import java.util.Properties;
+import java.util.Set;
+
 import net.i2p.data.DataFormatException;
+import net.i2p.router.startup.ClientAppConfig;
+import net.i2p.router.startup.LoadClientAppsJob;
 import net.i2p.util.Log;
 
+import org.mortbay.http.HttpListener;
+import org.mortbay.jetty.Server;
+
 /**
- *
+ *  Saves changes to clients.config or webapps.config
  */
 public class ConfigClientsHandler extends FormHandler {
     private Log _log;
     private Map _settings;
-    private boolean _shouldSave;
     
     public ConfigClientsHandler() {
-        _shouldSave = false;
+        _log = ContextHelper.getContext(null).logManager().getLog(ConfigClientsHandler.class);
     }
-    
+
     protected void processForm() {
-        if (_shouldSave) {
-            saveChanges();
+        if (_action.startsWith("Save Client")) {
+            saveClientChanges();
+        } else if (_action.startsWith("Save WebApp")) {
+            saveWebAppChanges();
+        } else if (_action.startsWith("Start ")) {
+            String app = _action.substring(6);
+            int appnum = -1;
+            try {
+                appnum = Integer.parseInt(app);
+            } catch (NumberFormatException nfe) {}
+            if (appnum >= 0)
+                startClient(appnum);
+            else
+                startWebApp(app);
         } else {
-            // noop
-            addFormError("Unimplemented");
+            addFormError("Unsupported " + _action);
         }
     }
     
-    public void setShouldsave(String moo) { 
-        if ( (moo != null) && (moo.equals("Save changes")) )
-            _shouldSave = true; 
-    }
-    
     public void setSettings(Map settings) { _settings = new HashMap(settings); }
     
-    /**
-     * The user made changes to the network config and wants to save them, so
-     * lets go ahead and do so.
-     *
-     */
-    private void saveChanges() {
-        _log = _context.logManager().getLog(ConfigClientsHandler.class);
-        boolean saveRequired = false;
-        
-        int updated = 0;
-        int index = 0;
-        
-        if (updated > 0)
-            addFormNotice("Updated settings");
-        
-        if (saveRequired) {
-            boolean saved = _context.router().saveConfig();
-            if (saved) 
-                addFormNotice("Exploratory tunnel configuration saved successfully");
-            else
-                addFormNotice("Error saving the configuration (applied but not saved) - please see the error logs");
+    private void saveClientChanges() {
+        List clients = ClientAppConfig.getClientApps(_context);
+        for (int cur = 0; cur < clients.size(); cur++) {
+            ClientAppConfig ca = (ClientAppConfig) clients.get(cur);
+            Object val = _settings.get(cur + ".enabled");
+            if (! "webConsole".equals(ca.clientName))
+                ca.disabled = val == null;
+        }
+        ClientAppConfig.writeClientAppConfig(_context, clients);
+        addFormNotice("Client configuration saved successfully - restart required to take effect");
+    }
+
+    private void startClient(int i) {
+        List clients = ClientAppConfig.getClientApps(_context);
+        if (i >= clients.size()) {
+            addFormError("Bad client index");
+            return;
+        }
+        ClientAppConfig ca = (ClientAppConfig) clients.get(i);
+        LoadClientAppsJob.runClient(ca.className, ca.clientName, LoadClientAppsJob.parseArgs(ca.args), _log);
+        addFormNotice("Client " + ca.clientName + " started");
+    }
+
+    private void saveWebAppChanges() {
+        Properties props = RouterConsoleRunner.webAppProperties();
+        Set keys = props.keySet();
+        int cur = 0;
+        for (Iterator iter = keys.iterator(); iter.hasNext(); ) {
+            String name = (String)iter.next();
+            if (! (name.startsWith(RouterConsoleRunner.PREFIX) && name.endsWith(RouterConsoleRunner.ENABLED)))
+                continue;
+            String app = name.substring(RouterConsoleRunner.PREFIX.length(), name.lastIndexOf(RouterConsoleRunner.ENABLED));
+            Object val = _settings.get(app + ".enabled");
+            if (! RouterConsoleRunner.ROUTERCONSOLE.equals(app))
+                props.setProperty(name, "" + (val != null));
+        }
+        RouterConsoleRunner.storeWebAppProperties(props);
+        addFormNotice("WebApp configuration saved successfully - restart required to take effect");
+    }
+
+    // Big hack for the moment, not using properties for directory and port
+    // Go through all the Jetty servers, find the one serving port 7657,
+    // requested and add the .war to that one
+    private void startWebApp(String app) {
+        Collection c = Server.getHttpServers();
+        for (int i = 0; i < c.size(); i++) {
+            Server s = (Server) c.toArray()[i];
+            HttpListener[] hl = s.getListeners();
+            for (int j = 0; j < hl.length; j++) {
+                if (hl[j].getPort() == 7657) {
+                    try {
+                        s.addWebApplication("/"+ app, "./webapps/" + app + ".war").start();
+                        // no passwords... initialize(wac);
+                        addFormNotice("WebApp " + app + " started");
+                    } catch (Exception ioe) {
+                        addFormError("Failed to start " + app + " " + ioe);
+                    }
+                    return;
+                }
+            }
         }
+        addFormError("Failed to find server");
     }
 }
diff --git a/apps/routerconsole/java/src/net/i2p/router/web/ConfigClientsHelper.java b/apps/routerconsole/java/src/net/i2p/router/web/ConfigClientsHelper.java
index 715899c208f1ca1ee27498637d7eecbf23f0feaa..a678b48a5a426030497da103f14ea780f9ec90da 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/ConfigClientsHelper.java
+++ b/apps/routerconsole/java/src/net/i2p/router/web/ConfigClientsHelper.java
@@ -26,16 +26,15 @@ public class ConfigClientsHelper {
 
     public ConfigClientsHelper() {}
     
-    
     public String getForm1() {
         StringBuffer buf = new StringBuffer(1024);
         buf.append("<table border=\"1\">\n");
-        buf.append("<tr><td>Client</td><td>Enabled?</td><td>Class and arguments</td></tr>\n");
+        buf.append("<tr><td>Client</td><td>Run at Startup?</td><td>Start Now</td><td>Class and arguments</td></tr>\n");
         
         List clients = ClientAppConfig.getClientApps(_context);
         for (int cur = 0; cur < clients.size(); cur++) {
             ClientAppConfig ca = (ClientAppConfig) clients.get(cur);
-            renderForm(buf, cur, ca.clientName, false, !ca.disabled, "webConsole".equals(ca.clientName), ca.className + " " + ca.args);
+            renderForm(buf, ""+cur, ca.clientName, false, !ca.disabled, "webConsole".equals(ca.clientName), ca.className + " " + ca.args);
         }
         
         buf.append("</table>\n");
@@ -45,24 +44,22 @@ public class ConfigClientsHelper {
     public String getForm2() {
         StringBuffer buf = new StringBuffer(1024);
         buf.append("<table border=\"1\">\n");
-        buf.append("<tr><td>WebApp</td><td>Enabled?</td><td>Description</td></tr>\n");
+        buf.append("<tr><td>WebApp</td><td>Run at Startup?</td><td>Start Now</td><td>Description</td></tr>\n");
         Properties props = RouterConsoleRunner.webAppProperties();
         Set keys = new TreeSet(props.keySet());
-        int cur = 0;
         for (Iterator iter = keys.iterator(); iter.hasNext(); ) {
             String name = (String)iter.next();
             if (name.startsWith(RouterConsoleRunner.PREFIX) && name.endsWith(RouterConsoleRunner.ENABLED)) {
-                String app = name.substring(8, name.lastIndexOf(RouterConsoleRunner.ENABLED));
+                String app = name.substring(RouterConsoleRunner.PREFIX.length(), name.lastIndexOf(RouterConsoleRunner.ENABLED));
                 String val = props.getProperty(name);
-                renderForm(buf, cur, app, !"addressbook".equals(app), "true".equals(val), RouterConsoleRunner.ROUTERCONSOLE.equals(app), app + ".war");
-                cur++;
+                renderForm(buf, app, app, !"addressbook".equals(app), "true".equals(val), RouterConsoleRunner.ROUTERCONSOLE.equals(app), app + ".war");
             }
         }
         buf.append("</table>\n");
         return buf.toString();
     }
 
-    private void renderForm(StringBuffer buf, int index, String name, boolean urlify, boolean enabled, boolean ro, String desc) {
+    private void renderForm(StringBuffer buf, String index, String name, boolean urlify, boolean enabled, boolean ro, String desc) {
         buf.append("<tr><td>");
         if (urlify && enabled) {
             String link = "/";
@@ -72,12 +69,16 @@ public class ConfigClientsHelper {
         } else {
             buf.append(name);
         }
-        buf.append("</td><td align=\"center\"><input type=\"checkbox\" name=\"enable\" value=\"").append(index).append(".enabled\" ");
+        buf.append("</td><td align=\"center\"><input type=\"checkbox\" name=\"").append(index).append(".enabled\" value=\"true\" ");
         if (enabled) {
             buf.append("checked=\"true\" ");
             if (ro)
                 buf.append("disabled=\"true\" ");
         }
-        buf.append("/><td>").append(desc).append("</td></tr>\n");
+        buf.append("/></td><td>&nbsp");
+        if (!enabled) {
+            buf.append("<button type=\"submit\" name=\"action\" value=\"Start ").append(index).append("\" />Start</button>");
+        }
+        buf.append("&nbsp</td><td>").append(desc).append("</td></tr>\n");
     }
 }
diff --git a/apps/routerconsole/java/src/net/i2p/router/web/SummaryHelper.java b/apps/routerconsole/java/src/net/i2p/router/web/SummaryHelper.java
index 0085d7b401c78caef4f18f5443f52ceefdd276ac..dec2d7891553eacbaf9f28c92301a3101f826fd0 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/SummaryHelper.java
+++ b/apps/routerconsole/java/src/net/i2p/router/web/SummaryHelper.java
@@ -10,6 +10,7 @@ import java.util.Set;
 import net.i2p.data.DataHelper;
 import net.i2p.data.Destination;
 import net.i2p.data.LeaseSet;
+import net.i2p.data.RouterAddress;
 import net.i2p.stat.Rate;
 import net.i2p.stat.RateStat;
 import net.i2p.router.CommSystemFacade;
@@ -17,6 +18,7 @@ import net.i2p.router.Router;
 import net.i2p.router.RouterContext;
 import net.i2p.router.RouterVersion;
 import net.i2p.router.TunnelPoolSettings;
+import net.i2p.router.transport.ntcp.NTCPAddress;
 
 /**
  * Simple helper to query the appropriate router for data necessary to render
@@ -120,7 +122,10 @@ public class SummaryHelper {
         int status = _context.commSystem().getReachabilityStatus();
         switch (status) {
             case CommSystemFacade.STATUS_OK:
-                return "OK";
+                RouterAddress ra = _context.router().getRouterInfo().getTargetAddress("NTCP");
+                if (ra == null || (new NTCPAddress(ra)).isPubliclyRoutable())
+                    return "OK";
+                return "ERR-Private TCP Address";
             case CommSystemFacade.STATUS_DIFFERENT:
                 return "ERR-SymmetricNAT";
             case CommSystemFacade.STATUS_REJECT_UNSOLICITED:
@@ -130,6 +135,8 @@ public class SummaryHelper {
                     return "WARN-Firewalled and Fast";
                 else
                     return "Firewalled";
+            case CommSystemFacade.STATUS_HOSED:
+                return "ERR-UDP Port In Use - Set i2np.udp.internalPort=xxxx in advanced config and restart";
             case CommSystemFacade.STATUS_UNKNOWN: // fallthrough
             default:
                 return "Testing";
@@ -512,6 +519,28 @@ public class SummaryHelper {
         return String.valueOf(_context.tunnelManager().getInboundBuildQueueSize());
     }
     
+    public String getPRNGStatus() {
+        Rate r = _context.statManager().getRate("prng.bufferWaitTime").getRate(60*1000);
+        int use = (int) r.getLastEventCount();
+        int i = (int) (r.getAverageValue() + 0.5);
+        if (i <= 0) {
+            r = _context.statManager().getRate("prng.bufferWaitTime").getRate(10*60*1000);
+            i = (int) (r.getAverageValue() + 0.5);
+        }
+        String rv = i + "/";
+        r = _context.statManager().getRate("prng.bufferFillTime").getRate(60*1000);
+        i = (int) (r.getAverageValue() + 0.5);
+        if (i <= 0) {
+            r = _context.statManager().getRate("prng.bufferFillTime").getRate(10*60*1000);
+            i = (int) (r.getAverageValue() + 0.5);
+        }
+        rv = rv + i + "ms";
+        // margin == fill time / use time
+        if (use > 0 && i > 0)
+            rv = rv + ' ' + (60*1000 / (use * i)) + 'x';
+        return rv;
+    }
+
     public boolean updateAvailable() { 
         return NewsFetcher.getInstance(_context).updateAvailable();
     }
diff --git a/apps/routerconsole/jsp/configclients.jsp b/apps/routerconsole/jsp/configclients.jsp
index 8f4f6c8e2733a27cc8614bb1944d7319156010cd..7240281988b926fed4dbfab859f521fd15a957e5 100644
--- a/apps/routerconsole/jsp/configclients.jsp
+++ b/apps/routerconsole/jsp/configclients.jsp
@@ -18,7 +18,6 @@
   
  <jsp:useBean class="net.i2p.router.web.ConfigClientsHandler" id="formhandler" scope="request" />
  <jsp:setProperty name="formhandler" property="contextId" value="<%=(String)session.getAttribute("i2p.contextId")%>" />
- <jsp:setProperty name="formhandler" property="shouldsave" value="<%=request.getParameter("shouldsave")%>" />
  <jsp:setProperty name="formhandler" property="action" value="<%=request.getParameter("action")%>" />
  <jsp:setProperty name="formhandler" property="nonce" value="<%=request.getParameter("nonce")%>" />
  <jsp:setProperty name="formhandler" property="settings" value="<%=request.getParameterMap()%>" />
@@ -30,7 +29,6 @@
     if (prev != null) System.setProperty("net.i2p.router.web.ConfigClientsHandler.noncePrev", prev);
     System.setProperty("net.i2p.router.web.ConfigClientsHandler.nonce", new java.util.Random().nextLong()+""); %>
  <input type="hidden" name="nonce" value="<%=System.getProperty("net.i2p.router.web.ConfigClientsHandler.nonce")%>" />
- <input type="hidden" name="action" value="blah" />
  <h3>Client Configuration</h3>
  <p>
  The Java clients listed below are started by the router and run in the same JVM.
@@ -39,7 +37,7 @@
  </p><p>
  <input type="submit" name="action" value="Save Client Configuration" />
  </p><p>
- <i>All changes require restart to take effect. For other changes edit the clients.config file.</i>
+ <i>All changes require restart to take effect. To change other client options, edit the clients.config file.</i>
  </p>
  <hr />
  <h3>WebApp Configuration</h3>
@@ -50,11 +48,15 @@
  front-ends to another client or application which must be separately enabled (e.g. susidns, i2ptunnel),
  or have no web interface at all (e.g. addressbook).
  </p><p>
+ A web app may also be disabled by removing the .war file from the webapps directory;
+ however the .war file and web app will reappear when you update your router to a newer version,
+ so disabling the web app here is the preferred method.
+ </p><p>
  <jsp:getProperty name="clientshelper" property="form2" />
  </p><p>
  <input type="submit" name="action" value="Save WebApp Configuration" />
  </p><p>
- <i>All changes require restart to take effect. For other changes edit the webapps.config file.</i>
+ <i>All changes require restart to take effect. To change other webapp options, edit the webapps.config file.</i>
  </p>
  </form>
 </div>
diff --git a/apps/routerconsole/jsp/confignav.jsp b/apps/routerconsole/jsp/confignav.jsp
index 8625ca6877b87b0910fad00283c0e50f5fcb5359..7ab47f76be1956ccd754f6c1281e9fe5c37eefc7 100644
--- a/apps/routerconsole/jsp/confignav.jsp
+++ b/apps/routerconsole/jsp/confignav.jsp
@@ -4,10 +4,10 @@
  %>Service | <% } else { %><a href="configservice.jsp">Service</a> | <% }
  if (request.getRequestURI().indexOf("configupdate.jsp") != -1) {
  %>Update | <% } else { %><a href="configupdate.jsp">Update</a> | <% }
- if (request.getRequestURI().indexOf("configclients.jsp") != -1) {
- %>Clients | <% } else { %><a href="configclients.jsp">Clients</a> | <% }
  if (request.getRequestURI().indexOf("configtunnels.jsp") != -1) {
  %>Tunnels | <% } else { %><a href="configtunnels.jsp">Tunnels</a> | <% }
+ if (request.getRequestURI().indexOf("configclients.jsp") != -1) {
+ %>Clients | <% } else { %><a href="configclients.jsp">Clients</a> | <% }
  if (request.getRequestURI().indexOf("configlogging.jsp") != -1) {
  %>Logging | <% } else { %><a href="configlogging.jsp">Logging</a> | <% }
  if (request.getRequestURI().indexOf("configstats.jsp") != -1) {
diff --git a/apps/routerconsole/jsp/summary.jsp b/apps/routerconsole/jsp/summary.jsp
index 1f284c64baad8931ae75a7ce8328d3f8e2c1b1a4..7bd85aaf92b39ee26223f5e3513b27a62602dc28 100644
--- a/apps/routerconsole/jsp/summary.jsp
+++ b/apps/routerconsole/jsp/summary.jsp
@@ -95,6 +95,7 @@
  <b>Message delay:</b> <jsp:getProperty name="helper" property="messageDelay" /><br />
  <b>Tunnel lag:</b> <jsp:getProperty name="helper" property="tunnelLag" /><br />
  <b>Handle backlog:</b> <jsp:getProperty name="helper" property="inboundBacklog" /><br />
+ <b>PRNG wait/fill:</b> <jsp:getProperty name="helper" property="PRNGStatus" /><br />
  <b><jsp:getProperty name="helper" property="tunnelStatus" /></b><br />
  <hr />
  
diff --git a/core/java/src/gnu/crypto/prng/AsyncFortunaStandalone.java b/core/java/src/gnu/crypto/prng/AsyncFortunaStandalone.java
index 90d8d97d73c0a7073bb27501e2aafad0c10187b4..c7b5c36e5d4895d91de8d14a0e8475085ec7e424 100644
--- a/core/java/src/gnu/crypto/prng/AsyncFortunaStandalone.java
+++ b/core/java/src/gnu/crypto/prng/AsyncFortunaStandalone.java
@@ -2,6 +2,8 @@ package gnu.crypto.prng;
 
 import java.util.*;
 
+import net.i2p.I2PAppContext;
+
 /**
  * fortuna instance that tries to avoid blocking if at all possible by using separate
  * filled buffer segments rather than one buffer (and blocking when that buffer's data
@@ -13,16 +15,20 @@ public class AsyncFortunaStandalone extends FortunaStandalone implements Runnabl
     private final byte asyncBuffers[][] = new byte[BUFFERS][BUFSIZE];
     private final int status[] = new int[BUFFERS];
     private int nextBuf = 0;
+    private I2PAppContext _context;
 
     private static final int STATUS_NEED_FILL = 0;
     private static final int STATUS_FILLING = 1;
     private static final int STATUS_FILLED = 2;
     private static final int STATUS_LIVE = 3;
     
-    public AsyncFortunaStandalone() {
+    public AsyncFortunaStandalone(I2PAppContext context) {
         super();
         for (int i = 0; i < BUFFERS; i++)
             status[i] = STATUS_NEED_FILL;
+        _context = context;
+        context.statManager().createRateStat("prng.bufferWaitTime", "", "Encryption", new long[] { 60*1000, 10*60*1000, 60*60*1000 } );
+        context.statManager().createRateStat("prng.bufferFillTime", "", "Encryption", new long[] { 60*1000, 10*60*1000, 60*60*1000 } );
     }
     
     public void startup() {
@@ -61,6 +67,7 @@ public class AsyncFortunaStandalone extends FortunaStandalone implements Runnabl
                 } catch (InterruptedException ie) {}
                 waited = System.currentTimeMillis()-before;
             }
+            _context.statManager().addRateData("prng.bufferWaitTime", waited, 0);
             if (waited > 10*1000)
                 System.out.println(Thread.currentThread().getName() + ": Took " + waited
                                    + "ms for a full PRNG buffer to be found");
@@ -108,6 +115,7 @@ public class AsyncFortunaStandalone extends FortunaStandalone implements Runnabl
                     //System.out.println(Thread.currentThread().getName() + ": Prng buffer " + toFill + " filled after " + (after-before));
                     asyncBuffers.notifyAll();
                 }
+                _context.statManager().addRateData("prng.bufferFillTime", after - before, 0);
                 Thread.yield();
                 long waitTime = (after-before)*5;
                 if (waitTime <= 0) // somehow postman saw waitTime show up as negative
@@ -147,7 +155,7 @@ public class AsyncFortunaStandalone extends FortunaStandalone implements Runnabl
     
     public static void main(String args[]) {
         try {
-            AsyncFortunaStandalone rand = new AsyncFortunaStandalone();
+            AsyncFortunaStandalone rand = new AsyncFortunaStandalone(null);  // Will cause NPEs above; fix this if you want to test! Sorry...
             
             byte seed[] = new byte[1024];
             rand.seed(seed);
diff --git a/core/java/src/net/i2p/stat/StatManager.java b/core/java/src/net/i2p/stat/StatManager.java
index 07cce24efb617a3fa97589a384549e9c837136b3..3f3ab4a7230c9fca05a2249849ae31a3b2f1e38f 100644
--- a/core/java/src/net/i2p/stat/StatManager.java
+++ b/core/java/src/net/i2p/stat/StatManager.java
@@ -37,13 +37,15 @@ public class StatManager {
     public static final String PROP_STAT_REQUIRED = "stat.required";
     /**
      * These are all the stats published in netDb, plus those required for the operation of
-     * the router (many in RouterThrottleImpl), plus those that are on graphs.jsp by default.
+     * the router (many in RouterThrottleImpl), plus those that are on graphs.jsp by default,
+     * plus those used on the summary bar (SummaryHelper.java).
      * Wildcard ('*') allowed at end of stat only.
      * Ignore all the rest of the stats unless stat.full=true.
      */
     public static final String DEFAULT_STAT_REQUIRED =
         "bw.recvRate,bw.sendBps,bw.sendRate,client.sendAckTime,clock.skew,crypto.elGamal.encrypt," +
         "jobQueue.jobLag,netDb.successTime,router.fastPeers," +
+        "prng.bufferFillTime,prng.bufferWaitTime," +
         "transport.receiveMessageSize,transport.sendMessageSize,transport.sendProcessingTime," +
         "tunnel.acceptLoad,tunnel.buildRequestTime,tunnel.rejectOverloaded,tunnel.rejectTimeout" +
         "tunnel.buildClientExpire,tunnel.buildClientReject,tunnel.buildClientSuccess," +
diff --git a/core/java/src/net/i2p/util/FortunaRandomSource.java b/core/java/src/net/i2p/util/FortunaRandomSource.java
index 2d1a6919659825d204801827f184ad4ea32c2ddb..865cc0bb44c36d7dbbfe26ae62c143c836f7a8b1 100644
--- a/core/java/src/net/i2p/util/FortunaRandomSource.java
+++ b/core/java/src/net/i2p/util/FortunaRandomSource.java
@@ -32,7 +32,7 @@ public class FortunaRandomSource extends RandomSource implements EntropyHarveste
 
     public FortunaRandomSource(I2PAppContext context) {
         super(context);
-        _fortuna = new AsyncFortunaStandalone();
+        _fortuna = new AsyncFortunaStandalone(context);
         byte seed[] = new byte[1024];
         if (initSeed(seed)) {
             _fortuna.seed(seed);
diff --git a/history.txt b/history.txt
index e202dd2064c1a5ab818e5437cc7a4c7e09d9d98b..a3e13b7e9dfdf8773cd8ae031f76cbeed0d0e579 100644
--- a/history.txt
+++ b/history.txt
@@ -1,3 +1,17 @@
+2008-06-23 zzz
+    * configclients.jsp: Add start button for clients and webapps.
+    * PRNG: Add two stats
+    * Summary bar:
+      - Display Warning for TCP private IP address
+      - Display PRNG stats
+    * OutNetMessage: Change cache logging from WARN to INFO
+
+2008-06-17 zzz
+    * Comm System: Add new STATUS_HOSED for use when UDP bind fails
+    * Summary bar: Display helpful errror message when UDP bind fails
+    * UDP: Don't bid when UDP bind fails
+    * configclients.jsp: Implement saves for clients and webapps.
+
 2008-06-16 zzz
     * UDP: Prevent 100% CPU when UDP bind fails;
       change bind fail message from ERROR to CRIT
@@ -12,7 +26,6 @@
     * configclients.jsp: New. For both clients and webapps.
       Saves are not yet implemented.
 
-
 2008-06-10 zzz
     * Floodfill: Add new FloodfillMonitorJob, which tracks active
       floodfills, and automatically enables/disables floodfill on
diff --git a/router/java/src/net/i2p/router/CommSystemFacade.java b/router/java/src/net/i2p/router/CommSystemFacade.java
index 2978ce9286206977dad7328518089a39fa179d84..3fb14186547cd030f6c85d7a5efd377366e2643f 100644
--- a/router/java/src/net/i2p/router/CommSystemFacade.java
+++ b/router/java/src/net/i2p/router/CommSystemFacade.java
@@ -61,6 +61,9 @@ public abstract class CommSystemFacade implements Service {
      */
     public void notifyReplaceAddress(RouterAddress UDPAddr) {}
     /** 
+     * These must be increasing in "badness" (see TransportManager.java),
+     * but UNKNOWN must be last.
+     *
      * We are able to receive unsolicited connections
      */
     public static final short STATUS_OK = 0;
@@ -75,10 +78,14 @@ public abstract class CommSystemFacade implements Service {
      * cannot receive unsolicited connections
      */
     public static final short STATUS_REJECT_UNSOLICITED = 2;
+    /**
+     * Our detection system is broken (SSU bind port failed)
+     */
+    public static final short STATUS_HOSED = 3;
     /**
      * Our reachability is unknown
      */
-    public static final short STATUS_UNKNOWN = 3;
+    public static final short STATUS_UNKNOWN = 4;
     
 }
 
diff --git a/router/java/src/net/i2p/router/RouterVersion.java b/router/java/src/net/i2p/router/RouterVersion.java
index da77a17e2f4600814eba8fed4e936b3eb8b34a40..9cea22605ce6224fc49a34d7acbae56bd94b66a7 100644
--- a/router/java/src/net/i2p/router/RouterVersion.java
+++ b/router/java/src/net/i2p/router/RouterVersion.java
@@ -17,7 +17,7 @@ import net.i2p.CoreVersion;
 public class RouterVersion {
     public final static String ID = "$Revision: 1.548 $ $Date: 2008-06-07 23:00:00 $";
     public final static String VERSION = "0.6.2";
-    public final static long BUILD = 3;
+    public final static long BUILD = 5;
     public static void main(String args[]) {
         System.out.println("I2P Router version: " + VERSION + "-" + BUILD);
         System.out.println("Router ID: " + RouterVersion.ID);
diff --git a/router/java/src/net/i2p/router/message/OutboundClientMessageOneShotJob.java b/router/java/src/net/i2p/router/message/OutboundClientMessageOneShotJob.java
index 8dba7275dc2bbecb7c21d1f3dc679fd4e8bb5396..02e2efd401cde7bd809161418428571bdad12d5a 100644
--- a/router/java/src/net/i2p/router/message/OutboundClientMessageOneShotJob.java
+++ b/router/java/src/net/i2p/router/message/OutboundClientMessageOneShotJob.java
@@ -247,8 +247,8 @@ public class OutboundClientMessageOneShotJob extends JobImpl {
                             return null;
                         //}
                     } else {
-                        if (_log.shouldLog(Log.WARN))
-                            _log.warn("Expired from cache - reply leaseset for " + _toString); 
+                        if (_log.shouldLog(Log.INFO))
+                            _log.info("Expired from cache - reply leaseset for " + _toString); 
                         // will get overwritten below
                         // _leaseSetCache.remove(hashPair());
                     }
@@ -256,8 +256,8 @@ public class OutboundClientMessageOneShotJob extends JobImpl {
             }
             _leaseSetCache.put(hashPair(), newLS);
         }
-        if (_log.shouldLog(Log.WARN))
-            _log.warn("Added to cache - reply leaseset for " + _toString); 
+        if (_log.shouldLog(Log.INFO))
+            _log.info("Added to cache - reply leaseset for " + _toString); 
         return newLS;
     }
     
@@ -329,8 +329,8 @@ public class OutboundClientMessageOneShotJob extends JobImpl {
                         }
                     }
                 }
-                if (_log.shouldLog(Log.WARN))
-                    _log.warn("Expired from cache - lease for " + _toString); 
+                if (_log.shouldLog(Log.INFO))
+                    _log.info("Expired from cache - lease for " + _toString); 
                 _leaseCache.remove(_to);
             }
         }
@@ -340,8 +340,8 @@ public class OutboundClientMessageOneShotJob extends JobImpl {
         for (int i = 0; i < _leaseSet.getLeaseCount(); i++) {
             Lease lease = _leaseSet.getLease(i);
             if (lease.isExpired(Router.CLOCK_FUDGE_FACTOR)) {
-                if (_log.shouldLog(Log.WARN))
-                    _log.warn(getJobId() + ": getNextLease() - expired lease! - " + lease + " for " + _toString);
+                if (_log.shouldLog(Log.INFO))
+                    _log.info(getJobId() + ": getNextLease() - expired lease! - " + lease + " for " + _toString);
                 continue;
             } else {
                 leases.add(lease);
@@ -403,8 +403,8 @@ public class OutboundClientMessageOneShotJob extends JobImpl {
         synchronized (_leaseCache) {
             _leaseCache.put(hashPair(), _lease);
         }
-        if (_log.shouldLog(Log.WARN))
-            _log.warn("Added to cache - lease for " + _toString); 
+        if (_log.shouldLog(Log.INFO))
+            _log.info("Added to cache - lease for " + _toString); 
         return true;
     }
 
diff --git a/router/java/src/net/i2p/router/startup/ClientAppConfig.java b/router/java/src/net/i2p/router/startup/ClientAppConfig.java
index 8c54ea1fa9effb471a172a9fe1e89fe09db5319e..0a994069f9234de2fc7cc1d06ea482616dc2eb64 100644
--- a/router/java/src/net/i2p/router/startup/ClientAppConfig.java
+++ b/router/java/src/net/i2p/router/startup/ClientAppConfig.java
@@ -2,6 +2,7 @@ package net.i2p.router.startup;
 
 import java.io.IOException;
 import java.io.File;
+import java.io.FileOutputStream;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Properties;
@@ -22,6 +23,7 @@ public class ClientAppConfig {
     
     private static final String PROP_CLIENT_CONFIG_FILENAME = "router.clientConfigFile";
     private static final String DEFAULT_CLIENT_CONFIG_FILENAME = "clients.config";
+    private static final String PREFIX = "clientApp.";
 
     // let's keep this really simple
     public String className;
@@ -63,14 +65,14 @@ public class ClientAppConfig {
         List rv = new ArrayList(5);
         int i = 0;
         while (true) {
-            String className = clientApps.getProperty("clientApp."+i+".main");
+            String className = clientApps.getProperty(PREFIX + i + ".main");
             if (className == null) 
                 break;
-            String clientName = clientApps.getProperty("clientApp."+i+".name");
-            String args = clientApps.getProperty("clientApp."+i+".args");
-            String delayStr = clientApps.getProperty("clientApp." + i + ".delay");
-            String onBoot = clientApps.getProperty("clientApp." + i + ".onBoot");
-            String disabled = clientApps.getProperty("clientApp." + i + ".startOnLoad");
+            String clientName = clientApps.getProperty(PREFIX + i + ".name");
+            String args = clientApps.getProperty(PREFIX + i + ".args");
+            String delayStr = clientApps.getProperty(PREFIX + i + ".delay");
+            String onBoot = clientApps.getProperty(PREFIX + i + ".onBoot");
+            String disabled = clientApps.getProperty(PREFIX + i + ".startOnLoad");
             i++;
             boolean dis = disabled != null && "false".equals(disabled);
 
@@ -87,5 +89,25 @@ public class ClientAppConfig {
         return rv;
     }
 
+    public static void writeClientAppConfig(RouterContext ctx, List apps) {
+        String clientConfigFile = ctx.getProperty(PROP_CLIENT_CONFIG_FILENAME, DEFAULT_CLIENT_CONFIG_FILENAME);
+        FileOutputStream fos = null;
+        try {
+            fos = new FileOutputStream(clientConfigFile);
+            StringBuffer buf = new StringBuffer(2048);
+            for(int i = 0; i < apps.size(); i++) {
+                ClientAppConfig app = (ClientAppConfig) apps.get(i);
+                buf.append(PREFIX).append(i).append(".main=").append(app.className).append("\n");
+                buf.append(PREFIX).append(i).append(".name=").append(app.clientName).append("\n");
+                buf.append(PREFIX).append(i).append(".args=").append(app.args).append("\n");
+                buf.append(PREFIX).append(i).append(".delay=").append(app.delay / 1000).append("\n");
+                buf.append(PREFIX).append(i).append(".startOnLoad=").append(!app.disabled).append("\n");
+            }
+            fos.write(buf.toString().getBytes());
+        } catch (IOException ioe) {
+        } finally {
+            if (fos != null) try { fos.close(); } catch (IOException ioe) {}
+        }
+    }
 }
 
diff --git a/router/java/src/net/i2p/router/startup/LoadClientAppsJob.java b/router/java/src/net/i2p/router/startup/LoadClientAppsJob.java
index 62e7132382f6733234849bf2d14b8b4b54eb742f..0770241d6b4f66886ef5efce791a3f6e9e25fafd 100644
--- a/router/java/src/net/i2p/router/startup/LoadClientAppsJob.java
+++ b/router/java/src/net/i2p/router/startup/LoadClientAppsJob.java
@@ -15,7 +15,7 @@ import net.i2p.util.Log;
  * it'll get queued up for starting 2 minutes later.
  *
  */
-class LoadClientAppsJob extends JobImpl {
+public class LoadClientAppsJob extends JobImpl {
     private Log _log;
     private static boolean _loaded = false;
     
@@ -36,7 +36,7 @@ class LoadClientAppsJob extends JobImpl {
             String argVal[] = parseArgs(app.args);
             if (app.delay == 0) {
                 // run this guy now
-                runClient(app.className, app.clientName, argVal);
+                runClient(app.className, app.clientName, argVal, _log);
             } else {
                 // wait before firing it up
                 getContext().jobQueue().addJob(new DelayedRunClient(getContext(), app.className, app.clientName, argVal, app.delay));
@@ -56,11 +56,11 @@ class LoadClientAppsJob extends JobImpl {
         }
         public String getName() { return "Delayed client job"; }
         public void runJob() {
-            runClient(_className, _clientName, _args);
+            runClient(_className, _clientName, _args, _log);
         }
     }
     
-    static String[] parseArgs(String args) {
+    public static String[] parseArgs(String args) {
         List argList = new ArrayList(4);
         if (args != null) {
             char data[] = args.toCharArray();
@@ -109,9 +109,9 @@ class LoadClientAppsJob extends JobImpl {
         return rv;
     }
 
-    private void runClient(String className, String clientName, String args[]) {
-        _log.info("Loading up the client application " + clientName + ": " + className + " " + args);
-        I2PThread t = new I2PThread(new RunApp(className, clientName, args));
+    public static void runClient(String className, String clientName, String args[], Log log) {
+        log.info("Loading up the client application " + clientName + ": " + className + " " + args);
+        I2PThread t = new I2PThread(new RunApp(className, clientName, args, log));
         if (clientName == null) 
             clientName = className + " client";
         t.setName(clientName);
@@ -119,17 +119,19 @@ class LoadClientAppsJob extends JobImpl {
         t.start();
     }
 
-    private final class RunApp implements Runnable {
+    private final static class RunApp implements Runnable {
         private String _className;
         private String _appName;
         private String _args[];
-        public RunApp(String className, String appName, String args[]) { 
+        private Log _log;
+        public RunApp(String className, String appName, String args[], Log log) { 
             _className = className; 
             _appName = appName;
             if (args == null)
                 _args = new String[0];
             else
                 _args = args;
+            _log = log;
         }
         public void run() {
             try {
diff --git a/router/java/src/net/i2p/router/transport/udp/UDPEndpoint.java b/router/java/src/net/i2p/router/transport/udp/UDPEndpoint.java
index cb57dcf0fcd2a7c986183d3af40f45d18a0f8a92..aefe18b8ba5d1084db4fc24b5ba6527a859b031d 100644
--- a/router/java/src/net/i2p/router/transport/udp/UDPEndpoint.java
+++ b/router/java/src/net/i2p/router/transport/udp/UDPEndpoint.java
@@ -5,6 +5,7 @@ import java.net.DatagramSocket;
 import java.net.InetAddress;
 import java.net.SocketException;
 
+import net.i2p.router.CommSystemFacade;
 import net.i2p.router.RouterContext;
 import net.i2p.util.Log;
 
@@ -44,6 +45,7 @@ public class UDPEndpoint {
             _sender.startup();
             _receiver.startup();
         } catch (SocketException se) {
+            _transport.setReachabilityStatus(CommSystemFacade.STATUS_HOSED);
             _log.log(Log.CRIT, "Unable to bind on port " + _listenPort, se);
         }
     }
diff --git a/router/java/src/net/i2p/router/transport/udp/UDPTransport.java b/router/java/src/net/i2p/router/transport/udp/UDPTransport.java
index 4e5e770304fa338a4f71b13623dff72cb7368b5d..040fac7615572004b35779e280aa5ba468e74a37 100644
--- a/router/java/src/net/i2p/router/transport/udp/UDPTransport.java
+++ b/router/java/src/net/i2p/router/transport/udp/UDPTransport.java
@@ -858,6 +858,12 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority
             else
                 return _fastBid;
         } else {
+            // If we don't have a port, all is lost
+            if ( _reachabilityStatus == CommSystemFacade.STATUS_HOSED) {
+                markUnreachable(to);
+                return null;
+            }
+
             // Validate his SSU address
             RouterAddress addr = toAddress.getTargetAddress(STYLE);
             if (addr == null) {
@@ -1870,6 +1876,7 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority
         buf.append(" </tr>\n");
         buf.append("<tr><td colspan=\"15\" valign=\"top\" align=\"left\">");
         long bytesTransmitted = _context.bandwidthLimiter().getTotalAllocatedOutboundBytes();
+        // NPE here early
         double averagePacketSize = _context.statManager().getRate("udp.sendPacketSize").getLifetimeAverageValue();
         // lifetime value, not just the retransmitted packets of current connections
         resentTotal = (long)_context.statManager().getRate("udp.packetsRetransmitted").getLifetimeEventCount();
@@ -2005,6 +2012,8 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority
                 break;
             case CommSystemFacade.STATUS_REJECT_UNSOLICITED:
                 _context.statManager().addRateData("udp.statusReject", 1, 0);
+                // fall through...
+            case CommSystemFacade.STATUS_HOSED:
                 _reachabilityStatus = status; 
                 _reachabilityStatusLastUpdated = now;
                 break;
@@ -2021,6 +2030,8 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority
                 break;
         }
         if ( (status != old) && (status != CommSystemFacade.STATUS_UNKNOWN) ) {
+            if (_log.shouldLog(Log.INFO))
+                _log.info("Old status: " + old + " New status: " + status + " from: ", new Exception("traceback"));
             if (needsRebuild())
                 rebuildExternalAddress();
         }