Improvements to the app management UI.

git-svn-id: https://svn.apache.org/repos/asf/tuscany/sca-cpp/trunk@1444660 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/hosting/server/apps.py b/hosting/server/apps.py
index 20c38cb..e61ee5e 100644
--- a/hosting/server/apps.py
+++ b/hosting/server/apps.py
@@ -25,7 +25,7 @@
     return ("apps", car(id), "app.info")
 
 # Put an app into the apps db
-def put(id, app, user, cache, dashboard, store, composites, pages, icons):
+def put(id, app, user, cache, db, dashboard, store, composites, pages, icons):
     debug('apps.py::put::id', id)
     debug('apps.py::put::app', app)
 
@@ -68,7 +68,7 @@
         return False
 
     # Clone app
-    appentry = mkentry(title(app), car(id), user.get(()), now(), content(app))
+    appentry = mkentry(title(capp), car(id), user.get(()), now(), content(capp))
     debug('apps.py::put::appentry', appentry)
     cache.put(appid(id), appentry)
     composites.put(id, composites.get((eid,)))
@@ -78,10 +78,19 @@
     return True
 
 # Get an app from the apps db
-def get(id, user, cache, dashboard, store, composites, pages, icons):
+def get(id, user, cache, db, dashboard, store, composites, pages, icons):
     debug('apps.py::get::id', id)
+
+    # Return the newest apps
     if isNull(id):
-        return (("'feed", ("'title", "Apps"), ("'id", "apps")),)
+        newentries = db.get((("'regex", '("apps" .* "app.info")'), ("'rank", "(regexp_matches(value, '(.*\(updated )([^\)]+)(\).*)'))[2]::timestamp"), ("'limit", 25)))
+        flatentries = tuple(map(lambda v: car(v), () if isNull(newentries) else newentries))
+        def sortkey(e):
+            return updated((e,))
+        sortedentries = tuple(sorted(flatentries, key = sortkey, reverse = True))[0:25]
+        newapps = ((("'feed", ("'title", "Apps"), ("'id", 'apps')) + sortedentries),)
+        debug('apps.py::get::newapps', newapps)
+        return newapps
 
     # Get the requested app
     app = cache.get(appid(id))
@@ -94,7 +103,7 @@
     return app
 
 # Delete an app from the apps db
-def delete(id, user, cache, dashboard, store, composites, pages, icons):
+def delete(id, user, cache, db, dashboard, store, composites, pages, icons):
     debug('apps.py::delete::id', id)
 
     # Get the requested app
diff --git a/hosting/server/composites.py b/hosting/server/composites.py
index d0276ab..581d202 100644
--- a/hosting/server/composites.py
+++ b/hosting/server/composites.py
@@ -43,7 +43,12 @@
     # Update the composite in the composite db
     compentry = mkentry(title(app), car(id), user.get(()), now(), content(comp))
     debug('composites.py::put::compentry', compentry)
-    return cache.put(compid(id), compentry)
+    rc = cache.put(compid(id), compentry)
+    if rc == False:
+        return False
+
+    # Update the app's updated date
+    return apps.put(id, app)
 
 # Get a composite from the composite db
 def get(id, user, cache, apps):
@@ -88,5 +93,10 @@
         return False
 
     # Delete the composite
-    return cache.delete(compid(id))
+    rc = cache.delete(compid(id))
+    if rc == False:
+        return False
+
+    # Update the app's updated date
+    return apps.put(id, app)
 
diff --git a/hosting/server/data/apps/nearme/app.info b/hosting/server/data/apps/nearme/app.info
index 07777a0..9669bd8 100644
--- a/hosting/server/data/apps/nearme/app.info
+++ b/hosting/server/data/apps/nearme/app.info
@@ -1 +1 @@
-((entry (title "nearme") (id "nearme") (author "admin@example.com") (updated "Jan 01, 2012") (content (info (description "Sample app")))))
+((entry (title "nearme") (id "nearme") (author "admin@example.com") (updated "2012-01-01T00:00:00+00:00") (content (info (description "Sample app")))))
diff --git a/hosting/server/data/apps/nearme2/app.info b/hosting/server/data/apps/nearme2/app.info
index e637eba..0b7f3f3 100644
--- a/hosting/server/data/apps/nearme2/app.info
+++ b/hosting/server/data/apps/nearme2/app.info
@@ -1 +1 @@
-((entry (title "nearme2") (id "nearme2") (author "admin@example.com") (updated "Jan 01, 2012") (content (info (description "Sample app")))))
+((entry (title "nearme2") (id "nearme2") (author "admin@example.com") (updated "2012-01-01T00:00:00+00:00") (content (info (description "Sample app")))))
diff --git a/hosting/server/data/apps/new/app.info b/hosting/server/data/apps/new/app.info
index 04ab8b8..3b5a1e7 100644
--- a/hosting/server/data/apps/new/app.info
+++ b/hosting/server/data/apps/new/app.info
@@ -1 +1 @@
-((entry (title "An empty app template") (id "new") (author "admin@example.com") (updated "Jan 01, 2012") (content (info (description "Sample app")))))
+((entry (title "A new app") (id "new") (author "admin@example.com") (updated "2012-01-01T00:00:00+00:00") (content (info (description "A new app")))))
diff --git a/hosting/server/data/apps/ourphotos/app.info b/hosting/server/data/apps/ourphotos/app.info
index afc57e8..7fd5c86 100644
--- a/hosting/server/data/apps/ourphotos/app.info
+++ b/hosting/server/data/apps/ourphotos/app.info
@@ -1 +1 @@
-((entry (title "Our photos of an event") (id "ourphotos") (author "admin@example.com") (updated "Jan 01, 2012") (content (info (description "Sample app")))))
+((entry (title "Our photos of an event") (id "ourphotos") (author "admin@example.com") (updated "2012-01-01T00:00:00+00:00") (content (info (description "Sample app")))))
diff --git a/hosting/server/data/apps/shoppingcart/app.info b/hosting/server/data/apps/shoppingcart/app.info
index 3b77112..c23fa1d 100644
--- a/hosting/server/data/apps/shoppingcart/app.info
+++ b/hosting/server/data/apps/shoppingcart/app.info
@@ -1 +1 @@
-((entry (title "My online store") (id "shoppingcart") (author "admin@example.com") (updated "Jan 01, 2012") (content (info (description "Sample app")))))
+((entry (title "My online store") (id "shoppingcart") (author "admin@example.com") (updated "2012-01-01T00:00:00+00:00") (content (info (description "Sample app")))))
diff --git a/hosting/server/data/apps/slice/app.info b/hosting/server/data/apps/slice/app.info
index 76685cd..02b7744 100644
--- a/hosting/server/data/apps/slice/app.info
+++ b/hosting/server/data/apps/slice/app.info
@@ -1 +1 @@
-((entry (title "Slice") (id "slice") (author "admin@example.com") (updated "Jan 01, 2012") (content (info (description "Sample app")))))
+((entry (title "Slice") (id "slice") (author "admin@example.com") (updated "2012-01-01T00:00:00+00:00") (content (info (description "Sample app")))))
diff --git a/hosting/server/data/apps/test/app.info b/hosting/server/data/apps/test/app.info
index b6ea414..90e3948 100644
--- a/hosting/server/data/apps/test/app.info
+++ b/hosting/server/data/apps/test/app.info
@@ -1 +1 @@
-((entry (title "An empty test app") (id "test") (author "admin@example.com") (updated "Jan 01, 2012") (content (info (description "Sample app")))))
+((entry (title "An empty test app") (id "test") (author "admin@example.com") (updated "2012-01-01T00:00:00+00:00") (content (info (description "Sample app")))))
diff --git a/hosting/server/data/apps/testanimation/app.info b/hosting/server/data/apps/testanimation/app.info
index a6759e4..9d3c35b 100644
--- a/hosting/server/data/apps/testanimation/app.info
+++ b/hosting/server/data/apps/testanimation/app.info
@@ -1 +1 @@
-((entry (title "Test animation components") (id "testanimation") (author "admin@example.com") (updated "Jan 01, 2012") (content (info (description "Sample app")))))
+((entry (title "Test animation components") (id "testanimation") (author "admin@example.com") (updated "2012-01-01T00:00:00+00:00") (content (info (description "Sample app")))))
diff --git a/hosting/server/data/apps/testdb/app.info b/hosting/server/data/apps/testdb/app.info
index 3f5b3de..df9d884 100644
--- a/hosting/server/data/apps/testdb/app.info
+++ b/hosting/server/data/apps/testdb/app.info
@@ -1 +1 @@
-((entry (title "Test database components") (id "testdb") (author "admin@example.com") (updated "Jan 01, 2012") (content (info (description "Sample app")))))
+((entry (title "Test database components") (id "testdb") (author "admin@example.com") (updated "2012-01-01T00:00:00+00:00") (content (info (description "Sample app")))))
diff --git a/hosting/server/data/apps/testevents/app.info b/hosting/server/data/apps/testevents/app.info
index 20191d6..788c083 100644
--- a/hosting/server/data/apps/testevents/app.info
+++ b/hosting/server/data/apps/testevents/app.info
@@ -1 +1 @@
-((entry (title "Test event components") (id "testevents") (author "admin@example.com") (updated "Jan 01, 2012") (content (info (description "Sample app")))))
+((entry (title "Test event components") (id "testevents") (author "admin@example.com") (updated "2012-01-01T00:00:00+00:00") (content (info (description "Sample app")))))
diff --git a/hosting/server/data/apps/testhttp/app.info b/hosting/server/data/apps/testhttp/app.info
index ef2afff..50f315e 100644
--- a/hosting/server/data/apps/testhttp/app.info
+++ b/hosting/server/data/apps/testhttp/app.info
@@ -1 +1 @@
-((entry (title "Test HTTP components") (id "testhttp") (author "admin@example.com") (updated "Jan 01, 2012") (content (info (description "Sample app")))))
+((entry (title "Test HTTP components") (id "testhttp") (author "admin@example.com") (updated "2012-01-01T00:00:00+00:00") (content (info (description "Sample app")))))
diff --git a/hosting/server/data/apps/testlogic/app.info b/hosting/server/data/apps/testlogic/app.info
index b4d3d9d..7475616 100644
--- a/hosting/server/data/apps/testlogic/app.info
+++ b/hosting/server/data/apps/testlogic/app.info
@@ -1 +1 @@
-((entry (title "Test logic components") (id "testlogic") (author "admin@example.com") (updated "Jan 01, 2012") (content (info (description "Sample app")))))
+((entry (title "Test logic components") (id "testlogic") (author "admin@example.com") (updated "2012-01-01T00:00:00+00:00") (content (info (description "Sample app")))))
diff --git a/hosting/server/data/apps/testsearch/app.info b/hosting/server/data/apps/testsearch/app.info
index 6c959dc..baf1659 100644
--- a/hosting/server/data/apps/testsearch/app.info
+++ b/hosting/server/data/apps/testsearch/app.info
@@ -1 +1 @@
-((entry (title "Test search components") (id "testsearch") (author "admin@example.com") (updated "Jan 01, 2012") (content (info (description "Sample app")))))
+((entry (title "Test search components") (id "testsearch") (author "admin@example.com") (updated "2012-01-01T00:00:00+00:00") (content (info (description "Sample app")))))
diff --git a/hosting/server/data/apps/testsms/app.info b/hosting/server/data/apps/testsms/app.info
index ef2afff..50f315e 100644
--- a/hosting/server/data/apps/testsms/app.info
+++ b/hosting/server/data/apps/testsms/app.info
@@ -1 +1 @@
-((entry (title "Test HTTP components") (id "testhttp") (author "admin@example.com") (updated "Jan 01, 2012") (content (info (description "Sample app")))))
+((entry (title "Test HTTP components") (id "testhttp") (author "admin@example.com") (updated "2012-01-01T00:00:00+00:00") (content (info (description "Sample app")))))
diff --git a/hosting/server/data/apps/testsocial/app.info b/hosting/server/data/apps/testsocial/app.info
index 16190d4..401a9a4 100644
--- a/hosting/server/data/apps/testsocial/app.info
+++ b/hosting/server/data/apps/testsocial/app.info
@@ -1 +1 @@
-((entry (title "Test social components") (id "testsocial") (author "admin@example.com") (updated "Jan 01, 2012") (content (info (description "Sample app")))))
+((entry (title "Test social components") (id "testsocial") (author "admin@example.com") (updated "2012-01-01T00:00:00+00:00") (content (info (description "Sample app")))))
diff --git a/hosting/server/data/apps/testtext/app.info b/hosting/server/data/apps/testtext/app.info
index e717015..4277aed 100644
--- a/hosting/server/data/apps/testtext/app.info
+++ b/hosting/server/data/apps/testtext/app.info
@@ -1 +1 @@
-((entry (title "Test text processing components") (id "testtext") (author "admin@example.com") (updated "Jan 01, 2012") (content (info (description "Sample app")))))
+((entry (title "Test text processing components") (id "testtext") (author "admin@example.com") (updated "2012-01-01T00:00:00+00:00") (content (info (description "Sample app")))))
diff --git a/hosting/server/data/apps/testurl/app.info b/hosting/server/data/apps/testurl/app.info
index 37d89b5..3b2d48f 100644
--- a/hosting/server/data/apps/testurl/app.info
+++ b/hosting/server/data/apps/testurl/app.info
@@ -1 +1 @@
-((entry (title "Test URL components") (id "testurl") (author "admin@example.com") (updated "Jan 01, 2012") (content (info (description "Sample app")))))
+((entry (title "Test URL components") (id "testurl") (author "admin@example.com") (updated "2012-01-01T00:00:00+00:00") (content (info (description "Sample app")))))
diff --git a/hosting/server/data/apps/testvalues/app.info b/hosting/server/data/apps/testvalues/app.info
index 42ed01b..97c9591 100644
--- a/hosting/server/data/apps/testvalues/app.info
+++ b/hosting/server/data/apps/testvalues/app.info
@@ -1 +1 @@
-((entry (title "Test values and lists") (id "testvalues") (author "admin@example.com") (updated "Jan 01, 2012") (content (info (description "Sample app")))))
+((entry (title "Test values and lists") (id "testvalues") (author "admin@example.com") (updated "2012-01-01T00:00:00+00:00") (content (info (description "Sample app")))))
diff --git a/hosting/server/data/apps/testwidgets/app.info b/hosting/server/data/apps/testwidgets/app.info
index 3d8e7be..3d0f35c 100644
--- a/hosting/server/data/apps/testwidgets/app.info
+++ b/hosting/server/data/apps/testwidgets/app.info
@@ -1 +1 @@
-((entry (title "Test widgets") (id "testwidgets") (author "admin@example.com") (updated "Jan 01, 2012") (content (info (description "Sample app")))))
+((entry (title "Test widgets") (id "testwidgets") (author "admin@example.com") (updated "2012-01-01T00:00:00+00:00") (content (info (description "Sample app")))))
diff --git a/hosting/server/data/apps/testwidgets2/app.info b/hosting/server/data/apps/testwidgets2/app.info
index 2ba2571..e5e2e47 100644
--- a/hosting/server/data/apps/testwidgets2/app.info
+++ b/hosting/server/data/apps/testwidgets2/app.info
@@ -1 +1 @@
-((entry (title "Test more widgets") (id "testwidgets2") (author "admin@example.com") (updated "Jan 01, 2012") (content (info (description "Sample app")))))
+((entry (title "Test more widgets") (id "testwidgets2") (author "admin@example.com") (updated "2012-01-01T00:00:00+00:00") (content (info (description "Sample app")))))
diff --git a/hosting/server/data/apps/testwidgets3/app.info b/hosting/server/data/apps/testwidgets3/app.info
index 3d01141..432d560 100644
--- a/hosting/server/data/apps/testwidgets3/app.info
+++ b/hosting/server/data/apps/testwidgets3/app.info
@@ -1 +1 @@
-((entry (title "Test HTML generator components") (id "testwidgets3") (author "admin@example.com") (updated "Jan 01, 2012") (content (info (description "Sample app")))))
+((entry (title "Test HTML generator components") (id "testwidgets3") (author "admin@example.com") (updated "2012-01-01T00:00:00+00:00") (content (info (description "Sample app")))))
diff --git a/hosting/server/data/apps/twsms/app.info b/hosting/server/data/apps/twsms/app.info
index d870caf..f1fe2b8 100644
--- a/hosting/server/data/apps/twsms/app.info
+++ b/hosting/server/data/apps/twsms/app.info
@@ -1 +1 @@
-((entry (title "SMS send service") (id "twsms") (author "admin@example.com") (updated "Jan 01, 2012") (content (info (description "Sample app")))))
+((entry (title "SMS send service") (id "twsms") (author "admin@example.com") (updated "2012-01-01T00:00:00+00:00") (content (info (description "Sample app")))))
diff --git a/hosting/server/data/store/all/store.apps b/hosting/server/data/store/all/store.apps
index cb12aa0..14b9390 100644
--- a/hosting/server/data/store/all/store.apps
+++ b/hosting/server/data/store/all/store.apps
@@ -1 +1 @@
-((feed (title "App Store") (id "all") (entry (title "Check my public social data") (id "me360") (author "admin@example.com") (updated "Jan 01, 2012")) (entry (title "Where are my friends") (id "nearme") (author "admin@example.com") (updated "Jan 01, 2012")) (entry (title "Where are my friends") (id "nearme2") (author "admin@example.com") (updated "Jan 01, 2012")) (entry (title "Our photos of an event") (id "ourphotos") (author "admin@example.com") (updated "Jan 01, 2012")) (entry (title "Slice") (id "slice") (author "admin@example.com") (updated "Jan 01, 2012")) (entry (title "My online store") (id "shoppingcart") (author "admin@example.com") (updated "Jan 01, 2012")) (entry (title "SMS send service") (id "twsms") (author "admin@example.com") (updated "Jan 01, 2012")) (entry (title "An empty test app") (id "test") (author "admin@example.com") (updated "Jan 01, 2012")) (entry (title "Test values and lists") (id "testvalues") (author "admin@example.com") (updated "Jan 01, 2012")) (entry (title "Test social components") (id "testsocial") (author "admin@example.com") (updated "Jan 01, 2012")) (entry (title "Test URL components") (id "testurl") (author "admin@example.com") (updated "Jan 01, 2012")) (entry (title "Test logic components") (id "testlogic") (author "admin@example.com") (updated "Jan 01, 2012")) (entry (title "Test text processing components") (id "testtext") (author "admin@example.com") (updated "Jan 01, 2012")) (entry (title "Test HTTP components") (id "testhttp") (author "admin@example.com") (updated "Jan 01, 2012")) (entry (title "Test SMS API") (id "testsms") (author "admin@example.com") (updated "Jan 01, 2012")) (entry (title "Test widgets") (id "testwidgets") (author "admin@example.com") (updated "Jan 01, 2012")) (entry (title "Test more widgets") (id "testwidgets2") (author "admin@example.com") (updated "Jan 01, 2012")) (entry (title "Test event components") (id "testevents") (author "admin@example.com") (updated "Jan 01, 2012")) (entry (title "Test search components") (id "testsearch") (author "admin@example.com") (updated "Jan 01, 2012")) (entry (title "Test database components") (id "testdb") (author "admin@example.com") (updated "Jan 01, 2012")) (entry (title "Test HTML generator components") (id "testwidgets3") (author "admin@example.com") (updated "Jan 01, 2012")) (entry (title "Test animation components") (id "testanimation") (author "admin@example.com") (updated "Jan 01, 2012"))))
+((feed (title "App Store") (id "all") (entry (title "Check my public social data") (id "me360") (author "admin@example.com") (updated "2012-01-01T00:00:00+00:00")) (entry (title "Where are my friends") (id "nearme") (author "admin@example.com") (updated "2012-01-01T00:00:00+00:00")) (entry (title "Where are my friends") (id "nearme2") (author "admin@example.com") (updated "2012-01-01T00:00:00+00:00")) (entry (title "Our photos of an event") (id "ourphotos") (author "admin@example.com") (updated "2012-01-01T00:00:00+00:00")) (entry (title "Slice") (id "slice") (author "admin@example.com") (updated "2012-01-01T00:00:00+00:00")) (entry (title "My online store") (id "shoppingcart") (author "admin@example.com") (updated "2012-01-01T00:00:00+00:00")) (entry (title "SMS send service") (id "twsms") (author "admin@example.com") (updated "2012-01-01T00:00:00+00:00")) (entry (title "An empty test app") (id "test") (author "admin@example.com") (updated "2012-01-01T00:00:00+00:00")) (entry (title "Test values and lists") (id "testvalues") (author "admin@example.com") (updated "2012-01-01T00:00:00+00:00")) (entry (title "Test social components") (id "testsocial") (author "admin@example.com") (updated "2012-01-01T00:00:00+00:00")) (entry (title "Test URL components") (id "testurl") (author "admin@example.com") (updated "2012-01-01T00:00:00+00:00")) (entry (title "Test logic components") (id "testlogic") (author "admin@example.com") (updated "2012-01-01T00:00:00+00:00")) (entry (title "Test text processing components") (id "testtext") (author "admin@example.com") (updated "2012-01-01T00:00:00+00:00")) (entry (title "Test HTTP components") (id "testhttp") (author "admin@example.com") (updated "2012-01-01T00:00:00+00:00")) (entry (title "Test SMS API") (id "testsms") (author "admin@example.com") (updated "2012-01-01T00:00:00+00:00")) (entry (title "Test widgets") (id "testwidgets") (author "admin@example.com") (updated "2012-01-01T00:00:00+00:00")) (entry (title "Test more widgets") (id "testwidgets2") (author "admin@example.com") (updated "2012-01-01T00:00:00+00:00")) (entry (title "Test event components") (id "testevents") (author "admin@example.com") (updated "2012-01-01T00:00:00+00:00")) (entry (title "Test search components") (id "testsearch") (author "admin@example.com") (updated "2012-01-01T00:00:00+00:00")) (entry (title "Test database components") (id "testdb") (author "admin@example.com") (updated "2012-01-01T00:00:00+00:00")) (entry (title "Test HTML generator components") (id "testwidgets3") (author "admin@example.com") (updated "2012-01-01T00:00:00+00:00")) (entry (title "Test animation components") (id "testanimation") (author "admin@example.com") (updated "2012-01-01T00:00:00+00:00"))))
diff --git a/hosting/server/data/store/featured/store.apps b/hosting/server/data/store/featured/store.apps
index adfcf5e..e196ddc 100644
--- a/hosting/server/data/store/featured/store.apps
+++ b/hosting/server/data/store/featured/store.apps
@@ -1 +1 @@
-((feed (title "App Store") (id "featured") (entry (title "Check my public social data") (id "me360") (author "admin@example.com") (updated "Jan 01, 2012")) (entry (title "Where are my friends") (id "nearme") (author "admin@example.com") (updated "Jan 01, 2012")) (entry (title "Where are my friends") (id "nearme2") (author "admin@example.com") (updated "Jan 01, 2012")) (entry (title "Our photos of an event") (id "ourphotos") (author "admin@example.com") (updated "Jan 01, 2012")) (entry (title "Slice") (id "slice") (author "admin@example.com") (updated "Jan 01, 2012")) (entry (title "My online store") (id "shoppingcart") (author "admin@example.com") (updated "Jan 01, 2012")) (entry (title "SMS send service") (id "twsms") (author "admin@example.com") (updated "Jan 01, 2012"))))
+((feed (title "App Store") (id "featured") (entry (title "Check my public social data") (id "me360") (author "admin@example.com") (updated "2012-01-01T00:00:00+00:00")) (entry (title "Where are my friends") (id "nearme") (author "admin@example.com") (updated "2012-01-01T00:00:00+00:00")) (entry (title "Where are my friends") (id "nearme2") (author "admin@example.com") (updated "2012-01-01T00:00:00+00:00")) (entry (title "Our photos of an event") (id "ourphotos") (author "admin@example.com") (updated "2012-01-01T00:00:00+00:00")) (entry (title "Slice") (id "slice") (author "admin@example.com") (updated "2012-01-01T00:00:00+00:00")) (entry (title "My online store") (id "shoppingcart") (author "admin@example.com") (updated "2012-01-01T00:00:00+00:00")) (entry (title "SMS send service") (id "twsms") (author "admin@example.com") (updated "2012-01-01T00:00:00+00:00"))))
diff --git a/hosting/server/data/store/new/store.apps b/hosting/server/data/store/new/store.apps
index b444d7f..b874c6e 100644
--- a/hosting/server/data/store/new/store.apps
+++ b/hosting/server/data/store/new/store.apps
@@ -1 +1 @@
-((feed (title "App Store") (id "new") (entry (title "Check my public social data") (id "me360") (author "admin@example.com") (updated "Jan 01, 2012")) (entry (title "Where are my friends") (id "nearme") (author "admin@example.com") (updated "Jan 01, 2012")) (entry (title "Where are my friends") (id "nearme2") (author "admin@example.com") (updated "Jan 01, 2012")) (entry (title "Our photos of an event") (id "ourphotos") (author "admin@example.com") (updated "Jan 01, 2012")) (entry (title "Slice") (id "slice") (author "admin@example.com") (updated "Jan 01, 2012")) (entry (title "My online store") (id "shoppingcart") (author "admin@example.com") (updated "Jan 01, 2012")) (entry (title "SMS send service") (id "twsms") (author "admin@example.com") (updated "Jan 01, 2012"))))
+((feed (title "App Store") (id "new") (entry (title "Check my public social data") (id "me360") (author "admin@example.com") (updated "2012-01-01T00:00:00+00:00")) (entry (title "Where are my friends") (id "nearme") (author "admin@example.com") (updated "2012-01-01T00:00:00+00:00")) (entry (title "Where are my friends") (id "nearme2") (author "admin@example.com") (updated "2012-01-01T00:00:00+00:00")) (entry (title "Our photos of an event") (id "ourphotos") (author "admin@example.com") (updated "2012-01-01T00:00:00+00:00")) (entry (title "Slice") (id "slice") (author "admin@example.com") (updated "2012-01-01T00:00:00+00:00")) (entry (title "My online store") (id "shoppingcart") (author "admin@example.com") (updated "2012-01-01T00:00:00+00:00")) (entry (title "SMS send service") (id "twsms") (author "admin@example.com") (updated "2012-01-01T00:00:00+00:00"))))
diff --git a/hosting/server/data/store/top/store.apps b/hosting/server/data/store/top/store.apps
index 63a7b34..5202823 100644
--- a/hosting/server/data/store/top/store.apps
+++ b/hosting/server/data/store/top/store.apps
@@ -1 +1 @@
-((feed (title "App Store") (id "top") (entry (title "Check my public social data") (id "me360") (author "admin@example.com") (updated "Jan 01, 2012")) (entry (title "Where are my friends") (id "nearme") (author "admin@example.com") (updated "Jan 01, 2012")) (entry (title "Where are my friends") (id "nearme2") (author "admin@example.com") (updated "Jan 01, 2012")) (entry (title "Our photos of an event") (id "ourphotos") (author "admin@example.com") (updated "Jan 01, 2012")) (entry (title "Slice") (id "slice") (author "admin@example.com") (updated "Jan 01, 2012")) (entry (title "My online store") (id "shoppingcart") (author "admin@example.com") (updated "Jan 01, 2012")) (entry (title "SMS send service") (id "twsms") (author "admin@example.com") (updated "Jan 01, 2012"))))
+((feed (title "App Store") (id "top") (entry (title "Check my public social data") (id "me360") (author "admin@example.com") (updated "2012-01-01T00:00:00+00:00")) (entry (title "Where are my friends") (id "nearme") (author "admin@example.com") (updated "2012-01-01T00:00:00+00:00")) (entry (title "Where are my friends") (id "nearme2") (author "admin@example.com") (updated "2012-01-01T00:00:00+00:00")) (entry (title "Our photos of an event") (id "ourphotos") (author "admin@example.com") (updated "2012-01-01T00:00:00+00:00")) (entry (title "Slice") (id "slice") (author "admin@example.com") (updated "2012-01-01T00:00:00+00:00")) (entry (title "My online store") (id "shoppingcart") (author "admin@example.com") (updated "2012-01-01T00:00:00+00:00")) (entry (title "SMS send service") (id "twsms") (author "admin@example.com") (updated "2012-01-01T00:00:00+00:00"))))
diff --git a/hosting/server/htdocs/account/index.html b/hosting/server/htdocs/account/index.html
index 9357a89..c16dde4 100644
--- a/hosting/server/htdocs/account/index.html
+++ b/hosting/server/htdocs/account/index.html
@@ -28,7 +28,7 @@
 <tr><tr><td class="label">Email:</td></tr>
 <tr><td><input type="text" id="userEmail" class="readentry" size="30" readonly="readonly" placeholder="Your email address" style="width: 300px;"/></td></tr>
 <tr><tr><td class="label">Picture:</td></tr>
-<tr><td><img id="userPicture" style="width: 50px; height: 50px; vertical-align: top;"/><input id="uploadPicture" type="button" class="lightbutton" value="Upload"/><input id="uploadFile" type="file" accept="image/*" style="display:none;"/><span id="refreshingPicture" class="refreshing" style="display:none;"/></td></tr>
+<tr><td><img id="userPicture" style="width: 50px; height: 50px; vertical-align: top;"/><input id="uploadPicture" type="button" class="lightbutton" value="Upload"/><input id="uploadFile" type="file" accept="image/*" style="visibility: hidden;"/><span id="refreshingPicture" class="refreshing" style="display:none;"/></td></tr>
 <tr><tr><td class="label">Name:</td></tr>
 <tr><td><input type="text" id="userFullname" class="flatentry" size="30" placeholder="Your name" style="width: 300px;"/></td></tr>
 <tr><tr><td class="label">Bio:</td></tr>
@@ -86,7 +86,7 @@
 (function layout() {
     document.title = config.windowtitle() + ' - Account';
     $('viewhead').innerHTML = '<span class="cmenu">' + username + '</span>' +
-        '<input type="button" class="redbutton plusminus" style="position: absolute; top: 4px; left: 5px;" id="deleteUser" value="-" title="Delete your account" disabled="true"/>';
+        '<input type="button" class="redbutton plusminus" style="position: absolute; top: 4px; left: 2px;" id="deleteUser" value="-" title="Delete your account" disabled="true"/>';
     if (!ui.isMobile())
         $('viewform').className = 'viewform flatscrollbars';
     $('userName').value = username;
@@ -133,7 +133,7 @@
         var acct = cadr(assoc("'content", acctentry));
 
         var email = assoc("'email", acct);
-        $('userEmail').value = isNull(email) || isNull(cdr(email))? '' : cadr(email);
+        $('userEmail').value = isNull(email) || isNull(cdr(email))? (username.indexOf('@') != -1? username : '') : cadr(email);
 
         var desc = assoc("'description", acct);
         $('userDescription').innerHTML = isNull(desc) || isNull(cdr(desc))? '' : cadr(desc);
@@ -354,7 +354,7 @@
 $('userFullname').onkeyup = $('userDescription').onkeyup = function() {
     var t = new Date().getTime();
     lastkeyup = t;
-    ui.delay(function() {
+    ui.async(function() {
             return t == lastkeyup? onaccountchange() : true;
         }, 2000);
 };
@@ -395,7 +395,7 @@
             showstatus('Loaded');
 
             // Now upload it
-            ui.delay(function() {
+            ui.async(function() {
                 var picentry = mklist("'entry", mklist("'title", username), mklist("'id", username), mklist("'author", username), mklist("'content", mklist("'picture", mklist("'image", url))));
                 var entryxml = car(atom.writeATOMEntry(valuesToElements(mklist(picentry))));
                 if (savedpicxml == entryxml) {
@@ -429,7 +429,7 @@
 
         // Open the email app
         var mailto = safeb64encode('p/' + username + '/' + token);
-        ui.navigate('mailto:' + mailto + '@' + topdomainname(window.location.hostname) + '?subject=Email to upload&body=Paste picture here', '_self');
+        ui.navigate('mailto:' + mailto + '@' + topdomainname(window.location.hostname) + '?subject=Uploading picture&body=Paste picture here', '_self');
 
         // Refresh app icon
         refreshingpic = true;
@@ -440,11 +440,12 @@
 /**
  * Handle picture upload events.
  */
-$('uploadPicture').onclick = function() {
-    if (ui.isMobile())
-        return emailpicture();
-    return $('uploadFile').click();
-};
+ui.onclick($('uploadPicture'), function(e) {
+    debug('uploadPicture.onclick()');
+    if (ui.isMobile() && ((ui.isWebkit() && ui.browserVersion() < 6.0) || (ui.isAndroid() && ui.browserVersion() < 2.2)))
+        return ui.delay(function() { return emailpicture(); });
+    return ui.delay(function() { return $('uploadFile').click(); });
+});
 $('uploadFile').onchange = function(e) {
     return readpic(e.target.files);
 };
diff --git a/hosting/server/htdocs/app/index.html b/hosting/server/htdocs/app/index.html
index 9ff3a72..108a35e 100644
--- a/hosting/server/htdocs/app/index.html
+++ b/hosting/server/htdocs/app/index.html
@@ -28,11 +28,9 @@
 <script src="http://www.example.com:9998/target/target-script-min.js#anonymous"></script>
 -->
 <title></title>
-<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0"/> 
-<!--
+<meta name="viewport" content="user-scalable=no, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0"/> 
 <meta name="apple-mobile-web-app-capable" content="yes"/>
 <meta name="apple-mobile-web-app-status-bar-style" content="black"/>
--->
 <link rel="apple-touch-icon-precomposed" href="/public/touchicon.png"/>
 <base href="/"/>
 <script type="text/javascript">
@@ -311,7 +309,7 @@
 
                 // Define the stylesheet
                 if (s != '') {
-                    var esheet = ui.elementByID(contentdiv, 'style_' + e.id);
+                    var esheet = ui.elementByID(document, 'style_' + e.id);
                     if (isNull(esheet)) {
                         var nesheet = document.createElement('style');
                         nesheet.id = 'style_' + e.id;
@@ -434,7 +432,7 @@
         return e;
     }
 
-    map(updatewidget, filter(function(e) { return !isNull(e.id) && e.id.substring(0, 5) != 'page:'; }, nodeList(ui.elementByID(contentdiv, 'page').childNodes)));
+    map(updatewidget, filter(function(e) { return !isNull(e.id) && e.id.substring(0, 5) != 'page:'; }, nodeList(ui.elementByID(document, 'page').childNodes)));
     return true;
 }
 
@@ -464,18 +462,19 @@
     if (e.className == 'button') {
         var b = car(childElements(e));
         b.name = e.id;
-        b.onclick = function() { return buttonClickHandler(b.value, appname); };
+        ui.onclick(b, function(e) {
+            return buttonClickHandler(b.value, appname);
+        });
         return e;
     }
     if (e.className == 'link') {
         var l = car(childElements(e));
         var hr = l.href;
         if (hr.substring(0, 5) == 'link:' && hr.indexOf('://') == -1) {
-            var f = function(e) {
+            ui.onclick(l, function(e) {
                 e.preventDefault();
                 return buttonClickHandler(hr.substring(5), appname);
-            };
-            l.ontouchstart = l.onclick = f;
+            });
             l.href = 'javascript:void()';
         }
         return e;
@@ -609,7 +608,7 @@
         }
 
         // Setup the widgets
-        map(setupwidget, filter(function(e) { return !isNull(e.id); }, nodeList(ui.elementByID(contentdiv, 'page').childNodes)));
+        map(setupwidget, filter(function(e) { return !isNull(e.id); }, nodeList(ui.elementByID(document, 'page').childNodes)));
 
         // Get the app components
         var comps = scdl.components(compos);
@@ -720,7 +719,7 @@
         return append(nodeList(n.childNodes), reduce(append, mklist(), map(childrenList, nodeList(n.childNodes))));
     }
 
-    var args = map(queryarg, filter(function(e) { return !isNull(e.id) && !isNull(inputvalue(e)); }, childrenList(ui.elementByID(contentdiv, 'page'))));
+    var args = map(queryarg, filter(function(e) { return !isNull(e.id) && !isNull(inputvalue(e)); }, childrenList(ui.elementByID(document, 'page'))));
 
     // Append current location properties if known
     if (!isNull(geoposition)) {
diff --git a/hosting/server/htdocs/clone/index.html b/hosting/server/htdocs/clone/index.html
index e9de1be..208aea9 100644
--- a/hosting/server/htdocs/clone/index.html
+++ b/hosting/server/htdocs/clone/index.html
@@ -164,9 +164,9 @@
 /**
  * Cancel cloning an app.
  */
-$('cloneAppCancelButton').onclick = function() {
+ui.onclick($('cloneAppCancelButton'), function(e) {
     history.back();
-};
+});
 
 })();
 </script>
diff --git a/hosting/server/htdocs/create/index.html b/hosting/server/htdocs/create/index.html
index 3fc26b8..daae212 100644
--- a/hosting/server/htdocs/create/index.html
+++ b/hosting/server/htdocs/create/index.html
@@ -72,6 +72,7 @@
     apps.put(name, savedappxml, function(e) {
         if (e) {
             if (e.code && e.code == 404) {
+                alert('name taken');
                 errorstatus('App name is taken, please pick another name');
                 workingstatus(false);
                 return false;
@@ -115,6 +116,7 @@
     // Check reserved app names
     var reserved = mklist('account', 'app', 'cache', 'clone', 'create', 'delete', 'graph', 'home', 'login', 'new', 'page', 'proxy', 'public', 'private', 'info', 'store');
     if (!isNull(assoc(name, map(function(r) { return mklist(r, r); }, reserved)))) {
+        alert('invalid name');
         errorstatus('App name is taken, please pick another name');
         return false;
     }
@@ -129,9 +131,9 @@
 /**
  * Cancel creating an app.
  */
-$('createAppCancelButton').onclick = function() {
+ui.onclick($('createAppCancelButton'), function(e) {
     history.back();
-};
+});
 
 /**
  * Show the status.
diff --git a/hosting/server/htdocs/delete/index.html b/hosting/server/htdocs/delete/index.html
index 3855a09..d578842 100644
--- a/hosting/server/htdocs/delete/index.html
+++ b/hosting/server/htdocs/delete/index.html
@@ -138,9 +138,9 @@
 /**
  * Cancel cloning an app.
  */
-$('deleteAppCancelButton').onclick = function() {
+ui.onclick($('deleteAppCancelButton'), function(e) {
     history.back();
-};
+});
 
 })();
 </script>
diff --git a/hosting/server/htdocs/graph/index.html b/hosting/server/htdocs/graph/index.html
index 557f427..d239c3c 100644
--- a/hosting/server/htdocs/graph/index.html
+++ b/hosting/server/htdocs/graph/index.html
@@ -2055,7 +2055,7 @@
         return displaydata(t, '100%');
     });
 
-    ui.async(function hidegraphdiv() {
+    ui.delay(function hidegraphdiv() {
         graphdiv.style.display = 'none'
     });
     return true;
@@ -2071,7 +2071,7 @@
     graphdiv.style.display = 'block'
     gvisible = true;
     graph.compselect(gcomp, true, atitle, cvalue, ccopy, cdelete);
-    ui.async(function hideplaydiv() {
+    ui.delay(function hideplaydiv() {
         pdiv.style.display = 'none';
         pdiv.innerHTML = '';
     });
diff --git a/hosting/server/htdocs/home/index.html b/hosting/server/htdocs/home/index.html
index b5240bf..c586e54 100644
--- a/hosting/server/htdocs/home/index.html
+++ b/hosting/server/htdocs/home/index.html
@@ -32,7 +32,7 @@
 <input type="button" class="bluebutton" style="font-size: 21px; padding: 10px; height: 50px;" id="getstarted" title="Get Started" value="Get Started"/>
 
 <br/><br/>
-<div class="note">Requires Safari 5+, Chrome 11+, Firefox 4+ or IE 9+</div>
+<div class="note">Requires Safari 6+, Chrome 24+, Firefox 18+ or IE 10+</div>
 <br/>
 
 </div>
@@ -51,9 +51,9 @@
         $('viewcontent').className = 'viewcontent flatscrollbars';
 })();
 
-$('getstarted').onclick = function() {
+ui.onclick($('getstarted'), function(e) {
     return ui.navigate('/#view=store', '_view');
-};
+});
 
 /**
  * Display animation.
diff --git a/hosting/server/htdocs/index.html b/hosting/server/htdocs/index.html
index 7722f96..a77c595 100644
--- a/hosting/server/htdocs/index.html
+++ b/hosting/server/htdocs/index.html
@@ -28,11 +28,9 @@
 <script src="http://www.example.com:9998/target/target-script-min.js#anonymous"></script>
 -->
 <title></title>
-<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0"/> 
-<!--
+<meta name="viewport" content="user-scalable=no, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0"/> 
 <meta name="apple-mobile-web-app-capable" content="yes"/>
 <meta name="apple-mobile-web-app-status-bar-style" content="black"/>
--->
 <link rel="apple-touch-icon-precomposed" href="/public/touchicon.png"/>
 <base href="/"/>
 <script type="text/javascript">
@@ -174,6 +172,11 @@
 var storeidx = 0;
 
 /**
+ * The current search query.
+ */
+var searchquery = '';
+
+/**
  * Populate cache with app resources.
  */
 var appresources = [
@@ -329,19 +332,18 @@
      $('menu').innerHTML = ui.menubar(
         append(mklist(ui.menu('menuhome', 'Home', '/', '_view', view == 'home'),
                     ui.menu('menustore', 'Store', '/#view=store&category=' + storecat + '&idx=' + storeidx, '_view', view == 'store'),
-                    ui.menu('menusearch', 'Search', '/#view=search', '_view', view == 'search')),
+                    ui.menu('menusearch', 'Search', '/#view=search&q=' + searchquery, '_view', view == 'search')),
                 (isNull(appname) || appname == 'undefined')?
                     mklist() :
                     mklist(
-                        ui.menu('menuinfo', 'Info', '/#view=info&app=' + appname, '_view', view == 'info'),
-                        ui.menu('menupage', 'Edit', '/#view=page&app=' + appname, '_view', view == 'page')
                         /* TODO disabled for now
-                        ,
+                        ui.menu('menuinfo', 'Info', '/#view=info&app=' + appname, '_view', view == 'info'),
+                        ui.menu('menupage', 'Edit', '/#view=page&app=' + appname, '_view', view == 'page'),
                         ui.menu('menulogic', config.logic(), '/#view=graph&app=' + appname, '_view', view == 'graph'),
                         ui.menu('menurun', '<span class="greentext" style="font-weight: bold">Run!</span>', '/' + appname + '/', '_blank', false)
                         */
                         )),
-        (isNull(appname) || appname == 'undefined')? mklist(
+        (true || isNull(appname) || appname == 'undefined')? mklist(
             ui.menufunc('menusignout', 'Sign out', 'return logout();', false),
             ui.menu('menuaccount', 'Account', '/#view=account', '_view', view == 'account')) :
             mklist());
@@ -380,12 +382,16 @@
     var uri = '/' + view + '/';
     var idx = isNull(params['idx'])? 0 : parseInt(params['idx']);
 
-    // Track store category view
+    // Track store category
     if (view == 'store') {
         storecat = isNull(params['category'])? 'top' : params['category'];
         storeidx = idx;
     }
 
+    // Track search query
+    if (view == 'search')
+        searchquery = isNull(params['q'])? '' : params['q'];
+
     // Determine the transition to use
     var vtransition = uri == viewuri? (idx >= viewidx? 'left' : 'right') : viewtransition(viewuri, uri);
 
@@ -407,7 +413,7 @@
         // Prepare current view for transition out
         var ovdiv = viewdiv;
         if (!isNull(ovdiv)) {
-            ovdiv.skipNode = true;
+            ui.removeElementIDs(ovdiv);
             ovdiv.className = 'viewunloading3dm';
         }
 
@@ -416,7 +422,6 @@
         var vdoc = appcache.get(uri);
         vdiv.innerHTML = vdoc;
         $('viewcontainer').appendChild(vdiv);
-        map(ui.evalScript, ui.innerScripts(vdiv));
 
         ui.async(function mtransitionview() {
             // Transition the old view out
@@ -425,20 +430,23 @@
 
             // Transition the new view in
             vdiv.className = 'viewloaded3dm';
+
+            ui.async(function mtransitioneval() {
+                map(ui.evalScript, ui.innerScripts(vdiv));
+            });
         });
 
     } else {
         // Prepare current view for transition out
         var ovdiv = viewdiv;
         if (!isNull(ovdiv))
-            ovdiv.skipNode = true;
+            ui.removeElementIDs(ovdiv);
 
         // Load the requested doc into the view
         var vdiv = mkviewdiv('viewloading3d');
         var vdoc = appcache.get(uri);
         vdiv.innerHTML = vdoc;
         $('viewcontainer').appendChild(vdiv);
-        map(ui.evalScript, ui.innerScripts(vdiv));
 
         ui.async(function transitionview() {
             // Transition the new view in
@@ -447,6 +455,10 @@
             // Transition the old view out
             if (!isNull(ovdiv))
                 ovdiv.parentNode.removeChild(ovdiv);
+
+            ui.async(function mtransitioneval() {
+                map(ui.evalScript, ui.innerScripts(vdiv));
+            });
         });
     }
 
@@ -622,7 +634,7 @@
             //debug('appcache iframe loaded');
         };
 
-        ui.delay(function() {
+        ui.async(function() {
             $('installer').innerHTML = '<iframe src="/cache/" class="installer"></iframe>';
         });
 
@@ -639,10 +651,10 @@
         }
 
         //debug('cache-manifest changed, reloading');
-        ui.delay(function() {
+        ui.async(function() {
             workingstatus(true);
             showstatus('Updating');
-            ui.delay(function() {
+            ui.async(function() {
                 workingstatus(true);
                 showstatus('Updating');
                 map(function(res) {
diff --git a/hosting/server/htdocs/info/index.html b/hosting/server/htdocs/info/index.html
index 3038d37..5143808 100644
--- a/hosting/server/htdocs/info/index.html
+++ b/hosting/server/htdocs/info/index.html
@@ -26,7 +26,7 @@
 <tr><td class="label">URL:</td></tr>
 <tr><td><input type="text" id="appURL" class="readentry" size="30" readonly="readonly" placeholder="App URL" style="width: 300px;"/></td></tr>
 <tr><td class="label">Icon:</td></tr>
-<tr><td><img id="appIcon" style="width: 50px; height: 50px; vertical-align: top;"/><input id="uploadIcon" type="button" class="lightbutton" value="Upload" style="display:none;"/><input id="uploadFile" type="file" accept="image/*" style="display: none;"/><span id="refreshingIcon" class="refreshing" style="display:none;"/></td></tr>
+<tr><td><img id="appIcon" style="width: 50px; height: 50px; vertical-align: top;"/><input id="uploadIcon" type="button" class="lightbutton" value="Upload" style="display: none;"/><input id="uploadFile" type="file" accept="image/*" style="visibility: hidden;"/><span id="refreshingIcon" class="refreshing" style="display:none;"/></td></tr>
 <tr><td class="label">Author:</td></tr>
 <tr><td><img id="authorPicture" style="width: 50px; height: 50px; vertical-align: middle;"/><input type="text" id="appAuthor" class="readentry" size="30" readonly="readonly" placeholder="Author of the app" style="width: 248px;"/></td></tr>
 <tr><td class="label">Rating:</td></tr>
@@ -55,12 +55,11 @@
  */
 (function layout() {
     document.title = config.windowtitle() + ' - Info - ' + appname;
-    $('viewhead').innerHTML = '<span id="appname" class="cmenu">' + appname + '</span>' +
-        '<input type="button" class="redbutton plusminus" style="position: absolute; top: 4px; left: 5px;" id="deleteApp" value="-" title="Delete this app" disabled="true"/>' +
-        '<span style="position: absolute; top: 0px; right: 5px;">' +
-        '<input type="button" class="greenbutton" id="runApp" value="Run" title="Run this app"/>' +
-        '<input type="button" class="bluebutton" id="cloneApp" value="'+ config.clone() +'" title="' + config.clone() + ' this app"/>' +
-        '</span>';
+    $('viewhead').innerHTML = '<span id="appname" class="cmenu">' + appname + 
+        '<input type="button" class="redbutton plusminus" style="position: absolute; top: 4px; left: 2px;" id="deleteApp" value="-" title="Delete this app" disabled="true"/>' +
+        '<input type="button" class="bluebutton" id="editApp" style="position: absolute; top: 4px; right: 72px;" value="Edit" title="Edit this app" disabled="true"/>' +
+        '<input type="button" class="greenbutton plusminus" id="runApp" style="position: absolute; top: 4px; right: 37px;" value="&gt;" title="Run this app"/>' +
+        '<input type="button" class="bluebutton" style="position: absolute; top: 4px; right: 2px; font-size: 16px;" id="cloneApp" value="C" title="' + config.clone() + ' this app"/>';
     if (!ui.isMobile())
         $('viewform').className = 'viewform flatscrollbars';
     $('appURL').value = window.location.hostname + '/' + appname + '/';
@@ -133,9 +132,13 @@
             $('appDescription').className = 'flatentry';
             $('uploadIcon').style.display = 'inline';
             $('deleteApp').disabled = false;
-            $('deleteApp').onclick = function() {
+            $('editApp').disabled = false;
+            ui.onclick($('editApp'), function(e) {
+                return ui.navigate('/#view=page&app=' + appname, '_view');
+            });
+            ui.onclick($('deleteApp'), function(e) {
                 return ui.navigate('/#view=delete&app=' + appname, '_view');
-            }
+            });
             onlinestatus();
         } else {
             showstatus('Read only');
@@ -360,7 +363,7 @@
 $('appDescription').onkeyup = function() {
     var t = new Date().getTime();
     lastkeyup = t;
-    ui.delay(function() {
+    ui.async(function() {
             return t == lastkeyup? onappchange() : true;
         }, 2000);
 };
@@ -376,16 +379,16 @@
 /**
  * Handle Clone button event.
  */
-$('cloneApp').onclick = function() {
+ui.onclick($('cloneApp'), function(e) {
     return ui.navigate('/#view=clone&app=' + appname, '_view');
-};
+});
 
 /**
  * Handle Run button event.
  */
-$('runApp').onclick = function() {
+ui.onclick($('runApp'), function(e) {
     return ui.navigate('/' + appname + '/', '_blank');
-};
+});
 
 /**
  * Read and upload icon file.
@@ -417,7 +420,7 @@
             showstatus('Loaded');
 
             // Now upload it
-            ui.delay(function() {
+            ui.async(function() {
                 var iconentry = mklist("'entry", mklist("'title", appname), mklist("'id", appname), mklist("'author", username), mklist("'content", mklist("'icon", mklist("'image", url))));
                 var entryxml = car(atom.writeATOMEntry(valuesToElements(mklist(iconentry))));
                 if (savediconxml == entryxml) {
@@ -451,7 +454,7 @@
 
         // Open the email app
         var mailto = safeb64encode('i/' + appname + '/' + token);
-        ui.navigate('mailto:' + mailto + '@' + topdomainname(window.location.hostname) + '?subject=Email to upload&body=Paste icon here', '_self');
+        ui.navigate('mailto:' + mailto + '@' + topdomainname(window.location.hostname) + '?subject=Uploading icon&body=Paste icon here', '_self');
 
         // Refresh app icon
         refreshingicon = true;
@@ -462,11 +465,11 @@
 /**
  * Handle icon upload events.
  */
-$('uploadIcon').onclick = function() {
-    if (ui.isMobile())
-        return emailicon();
-    return $('uploadFile').click();
-};
+ui.onclick($('uploadIcon'), function(e) {
+    if (ui.isMobile() && ((ui.isWebkit() && ui.browserVersion() < 6.0) || (ui.isAndroid() && ui.browserVersion() < 2.2)))
+        return ui.delay(function() { return emailicon(); });
+    return ui.delay(function() { return $('uploadFile').click(); });
+});
 $('uploadFile').onchange = function(e) {
     return uploadicon(e.target.files);
 };
@@ -484,9 +487,9 @@
 /**
  * Handle rate button event.
  */
-$('rateApp').onclick = function() {
+ui.onclick($('rateApp'), function(e) {
     return ui.navigate('/#view=rate&app=' + appname, '_view');
-};
+});
 
 })();
 </script>
diff --git a/hosting/server/htdocs/login/index.html b/hosting/server/htdocs/login/index.html
index 93d47d3..10e7b34 100644
--- a/hosting/server/htdocs/login/index.html
+++ b/hosting/server/htdocs/login/index.html
@@ -28,11 +28,9 @@
 <script src="http://www.example.com:9998/target/target-script-min.js#anonymous"></script>
 -->
 <title>Sign in</title>
-<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0"/> 
-<!--
+<meta name="viewport" content="user-scalable=no, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0"/> 
 <meta name="apple-mobile-web-app-capable" content="yes"/>
 <meta name="apple-mobile-web-app-status-bar-style" content="black"/>
--->
 <link rel="apple-touch-icon-precomposed" href="/public/touchicon.png"/>
 <base href="/login/"/>
 <script type="text/javascript">
@@ -137,7 +135,7 @@
 <tr><td><span id="loginprompt" style="font-size: 16px;"></span></tr></td>
 <tr><td><input type="text" class="flatentry" name="httpd_username" value="" placeholder="Username or email"/></td></tr>
 <tr><td><input type="password" class="flatentry" name="httpd_password" value="" placeholder="Password"/></td></tr>
-<tr><td><input type="submit" class="bluebutton" style="font-size: 16px; line-height: 16px; padding: 6px; height: 32px" value="Sign in"/></td></tr>
+<tr><td><input type="submit" class="bluebutton" style="font-size: 16px; line-height: 16px;" value="Sign in"/></td></tr>
 </table>
 <input type="hidden" name="httpd_location" value="/"/>
 </form>
@@ -146,7 +144,7 @@
 <form name="facebookOAuth2Form" style="width: 100%;">
 <table style="width: 100%;">
 <tr><td><span style="font-size: 16px;">Sign in with your <span style="font-weight: bold;">Facebook</span> account</span></td></tr>
-<tr><td><input type="button" id="facebookOAuth2Signin" value="Sign in" class="bluebutton" style="font-size: 16px; line-height: 16px; padding: 6px; height: 32px"/></td></tr>
+<tr><td><input type="button" id="facebookOAuth2Signin" value="Sign in" class="bluebutton" style="font-size: 16px;"/></td></tr>
 </table>
 </form>
 <br/>
@@ -154,7 +152,7 @@
 <form name="googleOAuth2Form" style="width: 100%;">
 <table style="width: 100%;">
 <tr><td><span style="font-size: 16px;">Sign in with your <span style="font-weight: bold;" >Google</span> account</span></td></tr>
-<tr><td><input type="button" id="googleOAuth2Signin" value="Sign in" class="bluebutton" style="font-size: 16px; line-height: 16px; padding: 6px; height: 32px"/></td></tr>
+<tr><td><input type="button" id="googleOAuth2Signin" value="Sign in" class="bluebutton" style="font-size: 16px;"/></td></tr>
 </table>
 </form>
 <br/>
@@ -194,7 +192,7 @@
     $('loginprompt').innerHTML = config.loginprompt();
     document.title = config.windowtitle() + ' - Sign in';
     $('viewhead').innerHTML = '<span class="bcmenu">' + config.pagetitle() + '</span>' +
-        '<span class="rmenu"><input type="button" id="signUp" class="redbutton" style="font-size: 16px; line-height: 16px; padding: 6px; height: 32px" title="' + config.signuptitle() + '" value="Sign up"/></span>';
+        '<input type="button" id="signUp" class="redbutton" style="position: absolute; top: 4px; right: 2px; font-size: 16px;" title="' + config.signuptitle() + '" value="Sign up"/>';
     if (!ui.isMobile())
         $('viewcontent').className = 'viewcontent flatscrollbars';
     $('status').className = ui.isMobile()? 'status3dm' : 'status3d';
@@ -337,13 +335,13 @@
     return parms;
 }
 
-$('facebookOAuth2Signin').onclick = function() {
+ui.onclick($('facebookOAuth2Signin'), function(e) {
     return submitoauth2signin(withfacebook);
-};
+});
 
-$('googleOAuth2Signin').onclick = function() {
+ui.onclick($('googleOAuth2Signin'), function(e) {
     return submitoauth2signin(withgoogle);
-};
+});
 
 /**
  * Signin with a username and password.
@@ -356,9 +354,9 @@
 /**
  * Signup.
  */
-$('signUp').onclick = function submitsignup() {
+ui.onclick($('signUp'), function submitsignup(e) {
     ui.navigate('/public/notyet/', '_self');
-};
+});
 
 /**
  * Handle orientation change.
@@ -442,7 +440,7 @@
             //debug('appcache iframe loaded');
         };
 
-        ui.delay(function() {
+        ui.async(function() {
             $('installer').innerHTML = '<iframe src="/public/cache/" class="installer"></iframe>';
         });
 
@@ -459,10 +457,10 @@
         }
 
         //debug('cache-manifest changed, reloading');
-        ui.delay(function() {
+        ui.async(function() {
             workingstatus(true);
             showstatus('Updating');
-            ui.delay(function() {
+            ui.async(function() {
                 workingstatus(true);
                 showstatus('Updating');
                 map(function(res) {
diff --git a/hosting/server/htdocs/page/index.html b/hosting/server/htdocs/page/index.html
index ca89ed3..3834640 100644
--- a/hosting/server/htdocs/page/index.html
+++ b/hosting/server/htdocs/page/index.html
@@ -95,11 +95,12 @@
     document.title = config.windowtitle() + ' - Page - ' + appname;
 
     $('viewhead').innerHTML = '<span id="appTitle" class="cmenu">' + appname + '</span>' +
-    '<input type="button" id="deleteWidgetButton" title="Delete a Widget" class="redbutton plusminus" style="position: absolute; top: 4px; left: 5px;" disabled="true" value="-"/>' +
-    '<span style="position: absolute; top: 0px; left: 45px; right: 115px; padding: 0px; background: transparent;"><input id="widgetValue" type="text" value="" class="flatentry" title="Widget value" autocapitalize="off" placeholder="Value" style="position: absolute; left: 0px; top: 4px; width: 100%; display: none;" readonly="readonly"/></span>' +
-    '<input type="button" id="playPageButton" title="View page" class="greenbutton plusminus" style="position: absolute; top: 4px; right: 75px;" value="&gt;"/>' +
-    '<input type="button" id="copyWidgetButton" title="Copy a Widget" class="bluebutton" style="position: absolute; top: 4px; right: 40px; font-size: 16px;" disabled="true" value="C"/>' +
-    '<input type="button" id="addWidgetButton" title="Add a Widget" class="bluebutton plusminus" style="position: absolute; top: 4px; right: 5px;" disabled="true" value="+"/>';
+    '<input type="button" id="deleteWidgetButton" title="Delete a widget" class="redbutton plusminus" style="position: absolute; top: 4px; left: 2px;" disabled="true" value="-"/>' +
+    '<span style="position: absolute; top: 0px; left: 37px; right: 110px; padding: 0px; background: transparent;"><input id="widgetValue" type="text" value="" class="flatentry" title="Widget value" autocapitalize="off" placeholder="Value" style="position: absolute; left: 0px; top: 4px; width: 100%; display: none;" readonly="readonly"/></span>' +
+    '<input type="button" id="playPageButton" title="View page" class="greenbutton plusminus" style="position: absolute; top: 4px; right: 72px;" value="&gt;"/>' +
+    '<input type="button" id="copyWidgetButton" title="Copy a widget" class="bluebutton" style="position: absolute; top: 4px; right: 37px; font-size: 16px;" disabled="true" value="C"/>' +
+    '<input type="button" id="addWidgetButton" title="Add a widget" class="bluebutton plusminus" style="position: absolute; top: 4px; right: 2px;" disabled="true" value="+"/>';
+    //'<input type="button" id="appInfoButton" title="View app info" class="bluebutton" style="position: absolute; top: 4px; right: 2px; font-size: 16px;" value="i"/>';
 
     if (ui.isMobile()) {
         $('palettecontainer').className = 'palettecontainer3dm';
@@ -606,7 +607,7 @@
         $('paletteview').className = 'paletteloading3dm';
         $('paletteview').style.display = 'block';
         $('paletteview').visible = true;
-        ui.async(function transitionview() {
+        ui.delay(function transitionview() {
             $('paletteview').className = 'paletteloaded3dm';
         });
     } else {
@@ -624,7 +625,7 @@
     if (ui.isMobile()) {
         $('paletteview').className = 'paletteunloading3dm';
         $('paletteview').visible = false;
-        ui.async(function transitionview() {
+        ui.delay(function transitionview() {
             $('paletteview').className = 'paletteunloaded3dm';
         });
     } else {
@@ -675,7 +676,7 @@
             return save(newxml);
 
         // Autosave other changes after 1 second
-        ui.delay(function autosave() {
+        ui.async(function autosave() {
             if (savedxhtml == newxml) {
                 showstatus('Saved');
                 return false;
@@ -806,14 +807,6 @@
             moved = false;
             return onmousedown(e);
         };
-        $('pagediv').addEventListener('touchstart', function(e) {
-            //debug('ontouchstart');
-            mdown = true;
-            moveX = e.touches[0].clientX;
-            moveY = e.touches[0].clientY;
-            moved = false;
-            return onmousedown(e);
-        }, false);
         $('palettecontent').ontouchstart = function(e) {
             //debug('ontouchstart');
             mdown = true;
@@ -977,24 +970,6 @@
         return true;
     }
 
-    /*
-    if (!ui.isMobile()) {
-        $('pagediv').onclick = function(e) {
-            //debug('onclick');
-            moveX = e.clientX;
-            moveY = e.clientY;
-            return onclick(e);
-        };
-    } else {
-        window.onclick = function(e) {
-            //debug('onclick');
-            moveX = e.touches[0].clientX;
-            moveY = e.touches[0].clientY;
-            return onclick(e);
-        };
-    }
-    */
-
     /**
      * Handle field on change events.
      */
@@ -1009,16 +984,16 @@
     };
 
     // Handle add widget event.
-    $('addWidgetButton').onclick = function() {
+    ui.onclick($('addWidgetButton'), function(e) {
 
         // Show / hide the palette
         if ($('paletteview').visible)
             return hidepalette();
         return showpalette();
-    };
+    });
 
     // Handle delete event.
-    $('deleteWidgetButton').onclick = function() {
+    ui.onclick($('deleteWidgetButton'), function(e) {
         if (selected == null)
             return false;
 
@@ -1035,10 +1010,10 @@
         // Trigger page change event
         onpagechange(true);
         return false;
-    };
+    });
 
     // Handle copy event.
-    $('copyWidgetButton').onclick = function() {
+    ui.onclick($('copyWidgetButton'), function(e) {
         if (selected == null)
             return false;
         if (selected.id.substring(0, 8) == 'palette:')
@@ -1068,18 +1043,18 @@
         // Trigger page change event
         onpagechange(true);
         return false;
-    };
+    });
 
     /**
-    * Handle play page button event.
-    */
-    $('playPageButton').onclick = function() {
+     * Handle play page button event.
+     */
+    ui.onclick($('playPageButton'), function(e) {
 
         // Show / hide the page play frame
         if ($('playdiv').visible)
             return showeditor();
         return showplaying();
-    }
+    });
 
     // Show the editor
     showeditor();
@@ -1234,6 +1209,15 @@
 };
 
 /**
+ * Handle app info button event.
+ */
+/* Disabled for now.
+ui.onclick($('appInfoButton'), function(e) {
+    return ui.navigate('/#view=info&app=' + appname, '_view');
+});
+*/
+
+/**
  * Initialize the page editor.
  */
 mkeditor();
diff --git a/hosting/server/htdocs/proxy/public/oops/index.html b/hosting/server/htdocs/proxy/public/oops/index.html
index f7cdab9..6d3b8b2 100644
--- a/hosting/server/htdocs/proxy/public/oops/index.html
+++ b/hosting/server/htdocs/proxy/public/oops/index.html
@@ -28,11 +28,9 @@
 <script src="http://www.example.com:9998/target/target-script-min.js#anonymous"></script>
 -->
 <title>Oops</title>
-<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0"/> 
-<!--
+<meta name="viewport" content="user-scalable=no, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0"/> 
 <meta name="apple-mobile-web-app-capable" content="yes"/>
 <meta name="apple-mobile-web-app-status-bar-style" content="black"/>
--->
 <link rel="apple-touch-icon-precomposed" href="/proxy/public/touchicon.png"/>
 <base href="/proxy/public/oops/"/>
 <script type="text/javascript">
@@ -305,7 +303,7 @@
             //debug('appcache iframe loaded');
         };
 
-        ui.delay(function() {
+        ui.async(function() {
             $('installer').innerHTML = '<iframe src="/proxy/public/cache/" class="installer"></iframe>';
         });
 
@@ -322,9 +320,9 @@
         }
 
         //debug('cache-manifest changed, reloading');
-        ui.delay(function() {
+        ui.async(function() {
             showstatus('Updating');
-            ui.delay(function() {
+            ui.async(function() {
                 map(function(res) {
                     appcache.remove(res[0]);
                     appcache.get(res[0], 'remote');
diff --git a/hosting/server/htdocs/public/notauth/index.html b/hosting/server/htdocs/public/notauth/index.html
index cf5f346..2cfd72e 100644
--- a/hosting/server/htdocs/public/notauth/index.html
+++ b/hosting/server/htdocs/public/notauth/index.html
@@ -28,11 +28,9 @@
 <script src="http://www.example.com:9998/target/target-script-min.js#anonymous"></script>
 -->
 <title>Sorry</title>
-<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0"/> 
-<!--
+<meta name="viewport" content="user-scalable=no, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0"/> 
 <meta name="apple-mobile-web-app-capable" content="yes"/>
 <meta name="apple-mobile-web-app-status-bar-style" content="black"/>
--->
 <link rel="apple-touch-icon-precomposed" href="/public/touchicon.png"/>
 <base href="/public/notauth/"/>
 <script type="text/javascript">
@@ -304,7 +302,7 @@
             //debug('appcache iframe loaded');
         };
 
-        ui.delay(function() {
+        ui.async(function() {
             $('installer').innerHTML = '<iframe src="/public/cache/" class="installer"></iframe>';
         });
 
@@ -321,9 +319,9 @@
         }
 
         //debug('cache-manifest changed, reloading');
-        ui.delay(function() {
+        ui.async(function() {
             showstatus('Updating');
-            ui.delay(function() {
+            ui.async(function() {
                 map(function(res) {
                     appcache.remove(res[0]);
                     appcache.get(res[0], 'remote');
diff --git a/hosting/server/htdocs/public/notfound/index.html b/hosting/server/htdocs/public/notfound/index.html
index ba82ecb..728319d 100644
--- a/hosting/server/htdocs/public/notfound/index.html
+++ b/hosting/server/htdocs/public/notfound/index.html
@@ -28,11 +28,9 @@
 <script src="http://www.example.com:9998/target/target-script-min.js#anonymous"></script>
 -->
 <title>Page not found</title>
-<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0"/> 
-<!--
+<meta name="viewport" content="user-scalable=no, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0"/> 
 <meta name="apple-mobile-web-app-capable" content="yes"/>
 <meta name="apple-mobile-web-app-status-bar-style" content="black"/>
--->
 <link rel="apple-touch-icon-precomposed" href="/public/touchicon.png"/>
 <base href="/public/notfound/"/>
 <script type="text/javascript">
@@ -305,7 +303,7 @@
             //debug('appcache iframe loaded');
         };
 
-        ui.delay(function() {
+        ui.async(function() {
             $('installer').innerHTML = '<iframe src="/public/cache/" class="installer"></iframe>';
         });
 
@@ -322,9 +320,9 @@
         }
 
         //debug('cache-manifest changed, reloading');
-        ui.delay(function() {
+        ui.async(function() {
             showstatus('Updating');
-            ui.delay(function() {
+            ui.async(function() {
                 map(function(res) {
                     appcache.remove(res[0]);
                     appcache.get(res[0], 'remote');
diff --git a/hosting/server/htdocs/public/notyet/index.html b/hosting/server/htdocs/public/notyet/index.html
index 0597d10..02d8f84 100644
--- a/hosting/server/htdocs/public/notyet/index.html
+++ b/hosting/server/htdocs/public/notyet/index.html
@@ -28,11 +28,9 @@
 <script src="http://www.example.com:9998/target/target-script-min.js#anonymous"></script>
 -->
 <title>Page not found</title>
-<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0"/> 
-<!--
+<meta name="viewport" content="user-scalable=no, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0"/> 
 <meta name="apple-mobile-web-app-capable" content="yes"/>
 <meta name="apple-mobile-web-app-status-bar-style" content="black"/>
--->
 <link rel="apple-touch-icon-precomposed" href="/public/touchicon.png"/>
 <base href="/public/notyet/"/>
 <script type="text/javascript">
@@ -305,7 +303,7 @@
             //debug('appcache iframe loaded');
         };
 
-        ui.delay(function() {
+        ui.async(function() {
             $('installer').innerHTML = '<iframe src="/public/cache/" class="installer"></iframe>';
         });
 
@@ -322,9 +320,9 @@
         }
 
         //debug('cache-manifest changed, reloading');
-        ui.delay(function() {
+        ui.async(function() {
             showstatus('Updating');
-            ui.delay(function() {
+            ui.async(function() {
                 map(function(res) {
                     appcache.remove(res[0]);
                     appcache.get(res[0], 'remote');
diff --git a/hosting/server/htdocs/public/oops/index.html b/hosting/server/htdocs/public/oops/index.html
index 7884352..ba9f563 100644
--- a/hosting/server/htdocs/public/oops/index.html
+++ b/hosting/server/htdocs/public/oops/index.html
@@ -28,11 +28,9 @@
 <script src="http://www.example.com:9998/target/target-script-min.js#anonymous"></script>
 -->
 <title>Oops</title>
-<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0"/> 
-<!--
+<meta name="viewport" content="user-scalable=no, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0"/> 
 <meta name="apple-mobile-web-app-capable" content="yes"/>
 <meta name="apple-mobile-web-app-status-bar-style" content="black"/>
--->
 <link rel="apple-touch-icon-precomposed" href="/public/touchicon.png"/>
 <base href="/public/oops/"/>
 <script type="text/javascript">
@@ -304,7 +302,7 @@
             //debug('appcache iframe loaded');
         };
 
-        ui.delay(function() {
+        ui.async(function() {
             $('installer').innerHTML = '<iframe src="/public/cache/" class="installer"></iframe>';
         });
 
@@ -321,9 +319,9 @@
         }
 
         //debug('cache-manifest changed, reloading');
-        ui.delay(function() {
+        ui.async(function() {
             showstatus('Updating');
-            ui.delay(function() {
+            ui.async(function() {
                 map(function(res) {
                     appcache.remove(res[0]);
                     appcache.get(res[0], 'remote');
diff --git a/hosting/server/htdocs/rate/index.html b/hosting/server/htdocs/rate/index.html
index 90d45bd..f5211ca 100644
--- a/hosting/server/htdocs/rate/index.html
+++ b/hosting/server/htdocs/rate/index.html
@@ -82,14 +82,14 @@
  * Initialize the rate buttons.
  */
 var rateAppButtons = [
-    [$('rateApp1'), 1, function() { return onclickrating(1); }, 'Don\'t like it'],
-    [$('rateApp2'), 2, function() { return onclickrating(2); }, 'It\'s ok'],
-    [$('rateApp3'), 3, function() { return onclickrating(3); }, 'It\'s good'],
-    [$('rateApp4'), 4, function() { return onclickrating(4); }, 'It\'s great']
+    [$('rateApp1'), 1, function(e) { return onclickrating(1); }, 'Don\'t like it'],
+    [$('rateApp2'), 2, function(e) { return onclickrating(2); }, 'It\'s ok'],
+    [$('rateApp3'), 3, function(e) { return onclickrating(3); }, 'It\'s good'],
+    [$('rateApp4'), 4, function(e) { return onclickrating(4); }, 'It\'s great']
 ];
 (function initRateAppButtons() {
     map(function(b) {
-        b[0].onclick = b[2];
+        ui.onclick(b[0], b[2]);
     }, rateAppButtons);
 })();
 
@@ -180,9 +180,9 @@
 /**
  * Navigate back.
  */
-$('rateAppDoneButton').onclick = function() {
+ui.onclick($('rateAppDoneButton'), function(e) {
     history.back();
-};
+});
 
 })();
 </script>
diff --git a/hosting/server/htdocs/search/index.html b/hosting/server/htdocs/search/index.html
index d46b052..d50b492 100644
--- a/hosting/server/htdocs/search/index.html
+++ b/hosting/server/htdocs/search/index.html
@@ -38,8 +38,8 @@
         $('viewcontent').className = 'viewcontent flatscrollbars';
 
     $('viewhead').innerHTML = '<form id="searchForm">' +
-    '<span style="position: absolute; top: 0px; left: 5px; right: 70px; padding: 0px; background: transparent;"><input type="text" id="searchQuery" value="" class="flatentry" title="Search" autocapitalize="off" placeholder="Search for apps" style="position: absolute; left: 0px; top: 4px; width: 100%;"></span>' +
-    '<input type="submit" id="searchButton" title="Search" class="bluebutton search" style="position: absolute; top: 4px; right: 5px; width: 60px; background-position: center center; background-repeat: no-repeat; background-image: url(\'' + ui.b64png(appcache.get('/public/search.b64')) + '\');" value=" "/>' +
+    '<span style="position: absolute; top: 0px; left: 2px; right: 70px; padding: 0px; background: transparent;"><input type="text" id="searchQuery" value="" class="flatentry" title="Search" autocapitalize="off" placeholder="Search for apps" style="position: absolute; left: 0px; top: 4px; width: 100%;"></span>' +
+    '<input type="submit" id="searchButton" title="Search" class="bluebutton search" style="position: absolute; top: 4px; right: 2px; width: 60px; background-position: center center; background-repeat: no-repeat; background-image: url(\'' + ui.b64png(appcache.get('/public/search.b64')) + '\');" value=" "/>' +
     '</form>';
 
     $('viewcontent').appendChild(ui.declareCSS(
@@ -50,6 +50,11 @@
 })();
 
 /**
+ * Get the requested search query.
+ */
+var query = ui.fragmentParams(location)['q'];
+
+/**
  * Initialize service references.
  */
 var editorComp = sca.component("Editor");
@@ -186,10 +191,16 @@
         return false;
     if (ui.isMobile())
         $('searchQuery').blur();
-    getapps($('searchQuery').value.trim());
+    //getapps($('searchQuery').value.trim());
+    ui.navigate('/#view=search&q=' + $('searchQuery').value.trim(), '_view');
     return false;
 };
 
+if(query && query != '') {
+    $('searchQuery').value = query;
+    getapps(query);
+}
+
 })();
 </script>
 
diff --git a/hosting/server/htdocs/store/index.html b/hosting/server/htdocs/store/index.html
index 57920ed..b64e3de 100644
--- a/hosting/server/htdocs/store/index.html
+++ b/hosting/server/htdocs/store/index.html
@@ -50,8 +50,7 @@
 var categories = [
     ['Featured', 'featured', 1],
     ['Top', 'top', 2],
-    //['New', 'new', 3],
-    //['Search', 'all', 4],
+    ['New', 'new', 3],
     ['My Apps', 'myapps', 5]
 ];
 
@@ -83,7 +82,7 @@
 
     var m = '';
     map(function(c) { m += catmenuitem(car(c), cadr(c), caddr(c)); }, categories);
-    m += '<span class="rmenu"><input type="button" class="bluebutton" id="createApp" title="Create a new app" Value="Create"/></span>';
+    m += '<input type="button" class="bluebutton" id="createApp" style="position: absolute; top: 4px; right: 2px;" title="Create a new app" Value="Create"/>';
     return m;
 })();
 
@@ -113,9 +112,9 @@
 /**
  * Create an app.
  */
-$('createApp').onclick = function() {
+ui.onclick($('createApp'), function(e) {
     return ui.navigate('/#view=create', '_view');
-};
+});
 
 /**
  * Get and display an app icon.
diff --git a/hosting/server/icons.py b/hosting/server/icons.py
index d9fbcab..a7d6334 100644
--- a/hosting/server/icons.py
+++ b/hosting/server/icons.py
@@ -105,12 +105,22 @@
         debug('icons.py::put::img', img)
         iconentry = mkentry(title(app), car(id), author(app), now(), ("'icon", ("'image", img)))
         debug('icons.py::put::iconentry', iconentry)
-        return cache.put(iconid(id), iconentry)
+        rc = cache.put(iconid(id), iconentry)
+        if rc == False:
+            return False
+
+        # Update the app's updated date
+        return apps.put(id, app)
 
     # Put default empty icon
     iconentry = mkentry(title(app), car(id), author(app), now(), ())
     debug('icons.py::put::iconentry', iconentry)
-    return cache.put(iconid(id), iconentry)
+    rc = cache.put(iconid(id), iconentry)
+    if rc == False:
+        return False
+
+    # Update the app's updated date
+    return apps.put(id, app)
 
 # Get an icon
 def get(id, user, cache, apps):
@@ -171,5 +181,10 @@
         return False
 
     # Delete the icon
-    return cache.delete(iconid(id))
+    rc = cache.delete(iconid(id))
+    if rc == False:
+        return False
+
+    # Update the app's updated date
+    return apps.put(id, app)
 
diff --git a/hosting/server/pages.py b/hosting/server/pages.py
index 3d35327..cb6a336 100644
--- a/hosting/server/pages.py
+++ b/hosting/server/pages.py
@@ -43,7 +43,12 @@
     # Update the page in the page db
     pageentry = mkentry(title(app), car(id), user.get(()), now(), content(page))
     debug('pages.py::put::pageentry', pageentry)
-    return cache.put(pageid(id), pageentry)
+    rc = cache.put(pageid(id), pageentry)
+    if rc == False:
+        return False
+
+    # Update the app's updated date
+    return apps.put(id, app)
 
 # Get a page from the page db
 def get(id, user, cache, apps):
@@ -90,5 +95,10 @@
         return False
 
     # Delete the page
-    return cache.delete(pageid(id))
+    rc = cache.delete(pageid(id))
+    if rc == False:
+        return False
+
+    # Update the app's updated date
+    return apps.put(id, app)
 
diff --git a/hosting/server/ratings.py b/hosting/server/ratings.py
index d36dcad..f1edeee 100644
--- a/hosting/server/ratings.py
+++ b/hosting/server/ratings.py
@@ -110,9 +110,9 @@
     if isNull(id):
         topentries = db.get((("'regex", '("ratings" .* "app.ratings")'), ("'rank", "(regexp_matches(value, '(.*\(rating )([^\)]+)(\).*)'))[2]::float"), ("'limit", 25)))
         flatentries = tuple(map(lambda v: car(v), () if isNull(topentries) else topentries))
-        def rating(e):
+        def sortkey(e):
             return cadr(assoc("'rating", assoc("'ratings", assoc("'content", e))))
-        sortedentries = tuple(sorted(flatentries, key = rating, reverse = True))
+        sortedentries = tuple(sorted(flatentries, key = sortkey, reverse = True))[0:25]
         topratings = ((("'feed", ("'title", "Ratings"), ("'id", 'ratings')) + sortedentries),)
         debug('ratings.py::get::topratings', topratings)
         return topratings
diff --git a/hosting/server/server.composite b/hosting/server/server.composite
index b3b95fb..51234a2 100644
--- a/hosting/server/server.composite
+++ b/hosting/server/server.composite
@@ -105,6 +105,7 @@
         <implementation.python script="apps.py"/>
         <reference name="user" target="User"/>
         <reference name="cache" target="Cache"/>
+        <reference name="db" target="Database"/>
         <reference name="dashboard" target="Dashboards"/>
         <reference name="store" target="AppStore"/>
         <reference name="composites" target="Composites"/>
diff --git a/hosting/server/store.py b/hosting/server/store.py
index aac233f..732e699 100644
--- a/hosting/server/store.py
+++ b/hosting/server/store.py
@@ -102,6 +102,14 @@
         debug('store.py::get::store', topstore)
         return topstore
 
+    # Collect the newest apps
+    if tag == 'new':
+        newapps = apps.get(()) 
+        newapps = mergeapps(cdddr(car(newapps)), apps, ratings)
+        newstore = ((("'feed", ("'title", 'App Store'), ("'id", tag)) + newapps),)
+        debug('store.py::get::store', newstore)
+        return newstore
+
     # Collect the featured apps
     appid = cdr(id)
     def findapp(appid, store):
diff --git a/hosting/server/test.py b/hosting/server/test.py
index 5670ec2..0e95c45 100755
--- a/hosting/server/test.py
+++ b/hosting/server/test.py
@@ -98,7 +98,7 @@
     # Put and get a page
     cache1 = mkcache('cache', {})
     page1updated = (("'entry", ("'title", 'app1'), ("'id", 'app1'), ("'author", 'jdoe@example.com'), ("'updated", '2012-01-01T00:00:00+00:00'), ("'content",)),)
-    assert pages.put(('app1',), page1, mkref('user', lambda id: 'jdoe@example.com'), cache1, mkref('apps', lambda id: app1)) == True
+    assert pages.put(('app1',), page1, mkref('user', lambda id: 'jdoe@example.com'), cache1, mkref('apps', lambda id, app = None: app1 if app is None else True)) == True
     assert pages.get(('app1',), mkref('user', lambda id: 'jdoe@example.com'), cache1, mkref('apps', lambda id: app1)) == page1updated
     
     # Reject put from user other than the author
@@ -113,7 +113,7 @@
     assert pages.get(('app1',), mkref('user', lambda id: 'jdoe@example.com'), cache1, mkref('apps', lambda id: app1)) == page1updated
 
     # Delete a page
-    assert pages.delete(('app1',), mkref('user', lambda id: 'jdoe@example.com'), cache1, mkref('apps', lambda id: app1)) == True
+    assert pages.delete(('app1',), mkref('user', lambda id: 'jdoe@example.com'), cache1, mkref('apps', lambda id, app = None: app1 if app is None else True)) == True
     assert pages.get(('app1',), mkref('user', lambda id: 'jdoe@example.com'), cache1, mkref('apps', lambda id: app1)) == defpagefromapp
     return True
 
@@ -137,7 +137,7 @@
     # Put and get a icon
     cache1 = mkcache('cache', {})
     icon1updated = (("'entry", ("'title", 'app1'), ("'id", 'app1'), ("'author", 'jdoe@example.com'), ("'updated", '2012-01-01T00:00:00+00:00'), ("'content", ("'icon", ("'image", img50)))),)
-    assert icons.put(('app1',), icon1, mkref('user', lambda id: 'jdoe@example.com'), cache1, mkref('apps', lambda id: app1)) == True
+    assert icons.put(('app1',), icon1, mkref('user', lambda id: 'jdoe@example.com'), cache1, mkref('apps', lambda id, app = None: app1 if app is None else True)) == True
     assert icons.get(('app1',), mkref('user', lambda id: 'jdoe@example.com'), cache1, mkref('apps', lambda id: app1)) == icon1updated
     
     # Reject put from user other than the author
@@ -164,11 +164,11 @@
 
     # Upload with valid token
     icon1oktoken = (("'entry", ("'title", 'app1'), ("'id", 'app1'), ("'author", 'jdoe@example.com'), ("'updated", '2012-01-01T00:00:00+00:00'), ("'content", ("'icon", ("'image", img50), ("'token", '1234')))),)
-    assert icons.put(('app1',), icon1oktoken, mkref('user', lambda id: 'jdoe@example.com'), cache1, mkref('apps', lambda id: app1)) == True
+    assert icons.put(('app1',), icon1oktoken, mkref('user', lambda id: 'jdoe@example.com'), cache1, mkref('apps', lambda id, app = None: app1 if app is None else True)) == True
     assert icons.get(('app1',), mkref('user', lambda id: 'jdoe@example.com'), cache1, mkref('apps', lambda id: app1)) == icon1updated
 
     # Delete a icon
-    assert icons.delete(('app1',), mkref('user', lambda id: 'jdoe@example.com'), cache1, mkref('apps', lambda id: app1)) == True
+    assert icons.delete(('app1',), mkref('user', lambda id: 'jdoe@example.com'), cache1, mkref('apps', lambda id, app = None: app1 if app is None else True)) == True
     assert icons.get(('app1',), mkref('user', lambda id: 'jdoe@example.com'), cache1, mkref('apps', lambda id: app1)) == deficonfromapp
     return True
 
@@ -187,7 +187,7 @@
     # Put and get a composite
     cache1 = mkcache('cache', {})
     composite1updated = (("'entry", ("'title", 'app1'), ("'id", 'app1'), ("'author", 'jdoe@example.com'), ("'updated", '2012-01-01T00:00:00+00:00'), ("'content",)),)
-    assert composites.put(('app1',), composite1, mkref('user', lambda id: 'jdoe@example.com'), cache1, mkref('apps', lambda id: app1)) == True
+    assert composites.put(('app1',), composite1, mkref('user', lambda id: 'jdoe@example.com'), cache1, mkref('apps', lambda id, app = None: app1 if app is None else True)) == True
     assert composites.get(('app1',), mkref('user', lambda id: 'jdoe@example.com'), cache1, mkref('apps', lambda id: app1)) == composite1updated
     
     # Reject put from user other than the author
@@ -202,36 +202,36 @@
     assert composites.get(('app1',), mkref('user', lambda id: 'jdoe@example.com'), cache1, mkref('apps', lambda id: app1)) == composite1updated
 
     # Delete a composite
-    assert composites.delete(('app1',), mkref('user', lambda id: 'jdoe@example.com'), cache1, mkref('apps', lambda id: app1)) == True
+    assert composites.delete(('app1',), mkref('user', lambda id: 'jdoe@example.com'), cache1, mkref('apps', lambda id, app = None: app1 if app is None else True)) == True
     assert composites.get(('app1',), mkref('user', lambda id: 'jdoe@example.com'), cache1, mkref('apps', lambda id: app1)) == defcompositefromapp
     return True
 
 def testApps():
     # Get default app
-    assert apps.get(('app1',), mkref('user', lambda id: 'jdoe@example.com'), mkcache('cache', {}), mkref('dashboard', lambda id: None), mkref('store', lambda id: None), mkref('composites', lambda id: None), mkref('pages', lambda id: None), mkref('icons', lambda id: None)) is None
+    assert apps.get(('app1',), mkref('user', lambda id: 'jdoe@example.com'), mkcache('cache', {}), mkref('db', lambda id: None), mkref('dashboard', lambda id: None), mkref('store', lambda id: None), mkref('composites', lambda id: None), mkref('pages', lambda id: None), mkref('icons', lambda id: None)) is None
 
     # Get an app
     app1 = (("'entry", ("'title", 'app1'), ("'id", 'app1'), ("'author", 'jdoe@example.com'), ("'updated", '2012-01-01T00:00:00+00:00'), ("'content", ("'info", ("'description", '')))),)
-    assert apps.get(('app1',), mkref('user', lambda id: 'jdoe@example.com'), mkcache('cache', {('apps', 'app1', 'app.info') : app1}), mkref('dashboard', lambda id: None), mkref('store', lambda id: None), mkref('composites', lambda id: None), mkref('pages', lambda id: None), mkref('icons', lambda id: None)) == app1
+    assert apps.get(('app1',), mkref('user', lambda id: 'jdoe@example.com'), mkcache('cache', {('apps', 'app1', 'app.info') : app1}), mkref('db', lambda id: None), mkref('dashboard', lambda id: None), mkref('store', lambda id: None), mkref('composites', lambda id: None), mkref('pages', lambda id: None), mkref('icons', lambda id: None)) == app1
 
     # Put and get an app
     cache1 = mkcache('cache', {})
-    assert apps.put(('app1',), app1, mkref('user', lambda id: 'jdoe@example.com'), cache1, mkref('dashboard', lambda id, app: True), mkref('store', lambda id, app: True), mkref('composites', lambda id, app: True), mkref('pages', lambda id, app: True), mkref('icons', lambda id, app: True)) == True
-    assert apps.get(('app1',), mkref('user', lambda id: 'jdoe@example.com'), cache1, mkref('dashboard', lambda id: None), mkref('store', lambda id: None), mkref('composites', lambda id: None), mkref('pages', lambda id: None), mkref('icons', lambda id: None)) == app1
+    assert apps.put(('app1',), app1, mkref('user', lambda id: 'jdoe@example.com'), cache1, mkref('db', lambda id: None), mkref('dashboard', lambda id, app: True), mkref('store', lambda id, app: True), mkref('composites', lambda id, app: True), mkref('pages', lambda id, app: True), mkref('icons', lambda id, app: True)) == True
+    assert apps.get(('app1',), mkref('user', lambda id: 'jdoe@example.com'), cache1, mkref('db', lambda id: None), mkref('dashboard', lambda id: None), mkref('store', lambda id: None), mkref('composites', lambda id: None), mkref('pages', lambda id: None), mkref('icons', lambda id: None)) == app1
     return True
     
     # Reject put from user other than the author
     app1otherauthor = (("'entry", ("'title", 'app1'), ("'id", 'app1'), ("'author", 'jane@example.com'), ("'updated", '2012-01-03T00:00:00+00:00'), ("'content",)),)
-    assert apps.put(('app1',), app1, mkref('user', lambda id: 'jane@example.com'), cache1, mkref('dashboard', lambda id, app: True), mkref('store', lambda id, app: True), mkref('composites', lambda id, app: True), mkref('pages', lambda id, app: True), mkref('icons', lambda id, app: True)) == false
-    assert apps.get(('app1',), mkref('user', lambda id: 'jane@example.com'), cache1, mkref('dashboard', lambda id: None), mkref('store', lambda id: None), mkref('composites', lambda id: None), mkref('pages', lambda id: None), mkref('icons', lambda id: None)) == app1
+    assert apps.put(('app1',), app1, mkref('user', lambda id: 'jane@example.com'), cache1, mkref('db', lambda id: None), mkref('dashboard', lambda id, app: True), mkref('store', lambda id, app: True), mkref('composites', lambda id, app: True), mkref('pages', lambda id, app: True), mkref('icons', lambda id, app: True)) == false
+    assert apps.get(('app1',), mkref('user', lambda id: 'jane@example.com'), cache1, mkref('db', lambda id: None), mkref('dashboard', lambda id: None), mkref('store', lambda id: None), mkref('composites', lambda id: None), mkref('pages', lambda id: None), mkref('icons', lambda id: None)) == app1
 
     # Reject delete from user other than the author
-    assert apps.delete(('app1',), mkref('user', lambda id: 'jane@example.com'), cache1, mkref('dashboard', lambda id: None), mkref('store', lambda id: None), mkref('composites', lambda id: None), mkref('pages', lambda id: None), mkref('icons', lambda id: None)) == False
-    assert apps.get(('app1',), mkref('user', lambda id: 'jane@example.com'), cache1, mkref('dashboard', lambda id: None), mkref('store', lambda id: None), mkref('composites', lambda id: None), mkref('pages', lambda id: None), mkref('icons', lambda id: None)) == app1
+    assert apps.delete(('app1',), mkref('user', lambda id: 'jane@example.com'), cache1, mkref('db', lambda id: None), mkref('dashboard', lambda id: None), mkref('store', lambda id: None), mkref('composites', lambda id: None), mkref('pages', lambda id: None), mkref('icons', lambda id: None)) == False
+    assert apps.get(('app1',), mkref('user', lambda id: 'jane@example.com'), cache1, mkref('db', lambda id: None), mkref('dashboard', lambda id: None), mkref('store', lambda id: None), mkref('composites', lambda id: None), mkref('pages', lambda id: None), mkref('icons', lambda id: None)) == app1
 
     # Delete an app
-    assert apps.delete(('app1',), mkref('user', lambda id: 'jdoe@example.com'), cache1, mkref('dashboard', lambda id: None), mkref('store', lambda id: None), mkref('composites', lambda id: None), mkref('pages', lambda id: None), mkref('icons', lambda id: None)) == True
-    assert apps.get(('app1',), mkref('user', lambda id: 'jdoe@example.com'), mkcache('cache', {}), mkref('dashboard', lambda id: None), mkref('store', lambda id: None), mkref('composites', lambda id: None), mkref('pages', lambda id: None), mkref('icons', lambda id: None)) is None
+    assert apps.delete(('app1',), mkref('user', lambda id: 'jdoe@example.com'), cache1, mkref('db', lambda id: None), mkref('dashboard', lambda id: None), mkref('store', lambda id: None), mkref('composites', lambda id: None), mkref('pages', lambda id: None), mkref('icons', lambda id: None)) == True
+    assert apps.get(('app1',), mkref('user', lambda id: 'jdoe@example.com'), mkcache('cache', {}), mkref('db', lambda id: None), mkref('dashboard', lambda id: None), mkref('store', lambda id: None), mkref('composites', lambda id: None), mkref('pages', lambda id: None), mkref('icons', lambda id: None)) is None
     return True
 
 def testStore():
diff --git a/modules/js/htdocs/component.js b/modules/js/htdocs/component.js
index a34ebfa..0a8ec5f 100644
--- a/modules/js/htdocs/component.js
+++ b/modules/js/htdocs/component.js
@@ -184,43 +184,113 @@
 };
 
 /**
- * Schedule async requests, and limit to 4 concurrent running requests.
+ * Schedule async requests, limiting the number of concurrent running requests.
  */
 HTTPBindingClient.queuedRequests = new Array();
-HTTPBindingClient.runningRequests = 0;
-HTTPBindingClient.scheduleAsyncRequest = function(f) {
+HTTPBindingClient.runningRequests = new Array();
+HTTPBindingClient.concurrentRequests = 2;
+
+HTTPBindingClient.scheduleAsyncRequest = function(f, cancelable) {
+    //debug('schedule async request, running ' + HTTPBindingClient.runningRequests.length + ' queued ' + HTTPBindingClient.queuedRequests.length);
+
     // Queue the request function
-    HTTPBindingClient.queuedRequests.push(f);
+    var req = new Object();
+    req.f = f;
+    req.cancelable = cancelable;
+    req.canceled = false;
+    HTTPBindingClient.queuedRequests.push(req);
 
-    // Execute requests in the queue
-    (function runAsyncRequests() {
-        // Stop now if we already have enough requests running or there's no request in the queue
-        //debug('runAsyncRequests', 'running', HTTPBindingClient.runningRequests, 'queued', HTTPBindingClient.queuedRequests.length);
-        if(HTTPBindingClient.runningRequests >= 4 || HTTPBindingClient.queuedRequests.length == 0)
+    // Execute any requests in the queue
+    setTimeout(function() {
+        HTTPBindingClient.runAsyncRequests(true);
+    }, 0);
+    return true;
+};
+
+HTTPBindingClient.forgetRequest = function(req) {
+    req.http = null;
+
+    // Remove a request from the list of running requests
+    for (i in HTTPBindingClient.runningRequests) {
+        if (HTTPBindingClient.runningRequests[i] == req) {
+            HTTPBindingClient.runningRequests.splice(i, 1);
+            //debug('forget async request, running ' + HTTPBindingClient.runningRequests.length + ' queued ' + HTTPBindingClient.queuedRequests.length);
             return true;
+        }
+    }
+    return false;
+};
 
-        // Run the first request in the queue
-        var req = HTTPBindingClient.queuedRequests.shift();
-        HTTPBindingClient.runningRequests++;
-        setTimeout(function runAsyncRequest() {
+HTTPBindingClient.cancelRequests = function() {
+    //debug('cancel async requests, running ' + HTTPBindingClient.runningRequests.length + ' queued ' + HTTPBindingClient.queuedRequests.length);
+
+    // Cancel any cancelable in flight HTTP requests
+    for (i in HTTPBindingClient.queuedRequests) {
+        var req = HTTPBindingClient.queuedRequests[i];
+        if (req.cancelable)
+            req.canceled = true;
+    }
+    for (i in HTTPBindingClient.runningRequests) {
+        var req = HTTPBindingClient.runningRequests[i];
+        if (req.cancelable) {
+            req.canceled = true;
+            if (req.http) {
+                req.http.abort();
+                req.http = null;
+                //debug('abort async request, running ' + HTTPBindingClient.runningRequests.length + ' queued ' + HTTPBindingClient.queuedRequests.length);
+            }
+        }
+    }
+
+    // Flush the queue
+    setTimeout(function() {
+        HTTPBindingClient.runAsyncRequests(true);
+    }, 0);
+}
+
+HTTPBindingClient.runAsyncRequests = function(fromui) {
+    // Stop now if we already have enough requests running or there's no request in the queue
+    if(HTTPBindingClient.runningRequests.length >= HTTPBindingClient.concurrentRequests || HTTPBindingClient.queuedRequests.length == 0)
+        return true;
+
+    // Run the first request in the queue
+    var req = HTTPBindingClient.queuedRequests.shift();
+    if (!req.canceled) {
+        HTTPBindingClient.runningRequests.push(req);
+        //debug('run async request, running ' + HTTPBindingClient.runningRequests.length + ' queued ' + HTTPBindingClient.queuedRequests.length);
+        var runAsyncRequest = function() {
+            if (req.canceled) {
+                HTTPBindingClient.forgetRequest(req);
+                //debug('canceled timed async request, running ' + HTTPBindingClient.runningRequests.length + ' queued ' + HTTPBindingClient.queuedRequests.length);
+                return false;
+            }
             try {
-                return req(function asyncRequestDone() {
+                req.http = new XMLHttpRequest();
+                return req.f(req.http, function asyncRequestDone() {
                     // Execute any requests left in the queue
-                    HTTPBindingClient.runningRequests--;
-                    runAsyncRequests();
+                    HTTPBindingClient.forgetRequest(req);
+                    //debug('done async request, running ' + HTTPBindingClient.runningRequests.length + ' queued ' + HTTPBindingClient.queuedRequests.length);
+                    HTTPBindingClient.runAsyncRequests(false);
                     return true;
                 });
             } catch(e) {
                 // Execute any requests left in the queue
-                HTTPBindingClient.runningRequests--;
-                runAsyncRequests();
+                HTTPBindingClient.forgetRequest(req);
+                //debug('err async request, running ' + HTTPBindingClient.runningRequests.length + ' queued ' + HTTPBindingClient.queuedRequests.length);
+                HTTPBindingClient.runAsyncRequests(false);
             }
-        }, 0);
+            return false;
+        };
+        if (false)
+            setTimeout(runAsyncRequest, 0);
+        else
+            runAsyncRequest();
+    } else {
+        //debug('canceled queued async request, running ' + HTTPBindingClient.runningRequests.length + ' queued ' + HTTPBindingClient.queuedRequests.length);
+    }
 
-        // Execute any requests left in the queue
-        runAsyncRequests();
-    })();
-    return true;
+    // Execute any requests left in the queue
+    HTTPBindingClient.runAsyncRequests(fromui);
 };
 
 /**
@@ -339,8 +409,7 @@
     // Call asynchronously with a callback
     if(hascb) {
         var u = this.uri;
-        return HTTPBindingClient.scheduleAsyncRequest(function jsonApplyRequest(done) {
-            var http = new XMLHttpRequest();
+        return HTTPBindingClient.scheduleAsyncRequest(function jsonApplyRequest(http, done) {
             http.open("POST", u, true);
             http.setRequestHeader("Accept", "*/*");
             http.setRequestHeader("Content-Type", "application/json-rpc");
@@ -371,7 +440,7 @@
             // Send the request
             http.send(req.data);
             return req.id;
-        });
+        }, false);
     }
 
     // Call synchronously and return the result or exception
@@ -402,16 +471,15 @@
                 return item;
 
             // Pass local result to callback
-            try {
+            setTimeout(function() {
                 cb(item);
-            } catch (cbe) {}
+            }, 0);
         }
     }
 
     // Call asynchronously with a callback
     if(hascb) {
-        return HTTPBindingClient.scheduleAsyncRequest(function getRequest(done) {
-            var http = new XMLHttpRequest();
+        return HTTPBindingClient.scheduleAsyncRequest(function getRequest(http, done) {
             http.open("GET", u, true);
             http.setRequestHeader("Accept", "*/*");
             http.onreadystatechange = function() {
@@ -420,7 +488,8 @@
                     // Pass result if different from local result
                     if(http.status == 200) {
                         var xl = http.getResponseHeader("X-Login");
-                        if(xl != null && xl != '') {
+                        var ct = http.getResponseHeader("Content-Type");
+                        if(xl != null && xl != '' && ct != null && ct.indexOf('text/html') == 0) {
                             // Detect redirect to a login page
                             try {
                                 var le = new HTTPBindingClient.Exception(403, 'X-Login');
@@ -431,7 +500,6 @@
                             } catch(cbe) {}
 
                         }
-                        var ct = http.getResponseHeader("Content-Type");
                         if(http.responseText == '' || ct == null || ct == '') {
                             // Report empty response
                             try {
@@ -467,7 +535,7 @@
             // Send the request
             http.send(null);
             return true;
-        });
+        }, true);
     }
 
     // Call synchronously and return the result or exception
@@ -477,7 +545,8 @@
     http.send(null);
     if(http.status == 200) {
         var xl = http.getResponseHeader("X-Login");
-        if(xl != null && xl != '') {
+        var ct = http.getResponseHeader("Content-Type");
+        if(xl != null && xl != '' && ct != null && ct.indexOf('text/html') == 0) {
             // Detect redirect to a login page
             var le = new HTTPBindingClient.Exception(403, 'X-Login');
             if(window.onloginredirect)
@@ -504,8 +573,7 @@
     // Call asynchronously with a callback
     if(hascb) {
         var u = this.uri;
-        return HTTPBindingClient.scheduleAsyncRequest(function postRequest(done) {
-            var http = new XMLHttpRequest();
+        return HTTPBindingClient.scheduleAsyncRequest(function postRequest(http, done) {
             http.open("POST", u, true);
             http.setRequestHeader("Accept", "*/*");
             http.setRequestHeader("Content-Type", "application/atom+xml");
@@ -529,7 +597,7 @@
             // Send the request
             http.send(entry);
             return true;
-        });
+        }, false);
     }
 
     // Call synchronously
@@ -562,8 +630,7 @@
 
     // Call asynchronously with a callback
     if(hascb) {
-        return HTTPBindingClient.scheduleAsyncRequest(function putRequest(done) {
-            var http = new XMLHttpRequest();
+        return HTTPBindingClient.scheduleAsyncRequest(function putRequest(http, done) {
             http.open("PUT", u, true);
             http.setRequestHeader("Accept", "*/*");
             http.setRequestHeader("Content-Type", "application/atom+xml");
@@ -599,7 +666,7 @@
             // Send the request
             http.send(entry);
             return true;
-        });
+        }, false);
     }
 
     // Call synchronously
@@ -640,8 +707,7 @@
 
     // Call asynchronously with a callback
     if(hascb) {
-        return HTTPBindingClient.scheduleAsyncRequest(function delRequest(done) {
-            var http = new XMLHttpRequest();
+        return HTTPBindingClient.scheduleAsyncRequest(function delRequest(http, done) {
             http.open("DELETE", u, true);        
             http.setRequestHeader("Accept", "*/*");
             http.onreadystatechange = function() {
@@ -665,7 +731,7 @@
             // Send the request
             http.send(null);
             return true;
-        });
+        }, false);
     }
 
     // Call synchronously
diff --git a/modules/js/htdocs/ui.js b/modules/js/htdocs/ui.js
index 3aa9e10..2bd21f6 100644
--- a/modules/js/htdocs/ui.js
+++ b/modules/js/htdocs/ui.js
@@ -29,6 +29,8 @@
 ui.elementByID = function(node, id) {
     if (node.skipNode == true)
         return null;
+    if (node == document)
+        return document.getElementById(id);
     for (var i in node.childNodes) {
         var child = node.childNodes[i];
         if (isNull(child))
@@ -43,6 +45,21 @@
 };
 
 /**
+ * Remove ids in a tree of elements.
+ */
+ui.removeElementIDs = function(node) {
+    if (!isNull(node.id))
+        node.id = null;
+    for (var i in node.childNodes) {
+        var child = node.childNodes[i];
+        if (isNull(child))
+            continue;
+        ui.removeElementIDs(child);
+    }
+    return true;
+};
+
+/**
  * Return the current document, or a child element with the given id.
  */
 function $(id) {
@@ -229,6 +246,20 @@
 };
 
 /**
+ * Return the Safari version.
+ */
+ui.browserVersion = function() {
+    return Number(navigator.userAgent.replace(/.*Version\/(\d+\.\d+).*/, '$1'));
+};
+
+/**
+ * Return true if the client is Android based.
+ */
+ui.isAndroid = function() {
+    return navigator.userAgent.match(/Android/i);
+};
+
+/**
  * Return true if the client is Firefox.
  */
 ui.isFirefox = function() {
@@ -273,16 +304,11 @@
 /**
  * Run a UI rendering function asynchronously.
  */
-ui.asyncFrame = null;
-ui.async = function(f) {
-    if (isNull(ui.asyncFrame))
-        // Use requestAnimationFrame when available, fallback to setTimeout
-        ui.asyncFrame = window.requestAnimationFrame || window.webkitRequestAnimationFrame ||
-            window.mozRequestAnimationFrame || window.oRequestAnimationFrame || window.msRequestAnimationFrame ||
-            function(f) {
-                return window.setTimeout(f, 16);
-            };
-    return ui.asyncFrame.call(window, f);
+ui.async = function(f, t) {
+    window.setTimeout(function() {
+        return f();
+    }, isNull(t)? 0 : t);
+    return true;
 };
 
 /**
@@ -290,10 +316,10 @@
  */
 ui.delayed = {}
 ui.delay = function(f, t) {
-    var id =  window.setTimeout(function() {
+    var id = window.setTimeout(function() {
         delete ui.delayed[id];
         return f();
-    }, isNull(t)? 16 : t);
+    }, isNull(t)? 0 : t);
     ui.delayed[id] = id;
     return id;
 };
@@ -443,6 +469,9 @@
             window.ontouchmove = null;
         }
 
+        // Cancel any cancelable HTTP requests
+        HTTPBindingClient.cancelRequests();
+
         // Cleanup memoized element lookups
         ui.unmemo$();
 
@@ -460,12 +489,65 @@
 }
 
 /**
+ * Bind a click handler to a widget.
+ */
+ui.ontouchstart = function(widget, e) {
+    //debug('ontouchstart');
+    widget.down = true;
+    widget.moved = false;
+    var t = e.touches[0];
+    widget.moveX = t.clientX;
+    widget.moveY = t.clientY;
+};
+
+ui.ontouchmove = function(widget, e) {
+    //debug('ontouchmove');
+    var t = e.touches[0];
+    if (t.clientX != widget.moveX) {
+        widget.moveX = t.clientX;
+        widget.moved = true;
+    }
+    if (t.clientY != widget.moveY) {
+        widget.moveY = t.clientY;
+        widget.moved = true;
+    }
+};
+
+ui.ontouchend = function(widget, e) {
+    //debug('ontouchend');
+    widget.down = false;
+    if (!widget.moved) {
+        e.preventDefault();
+        return widget.onclick(e);
+    }
+};
+
+ui.onclick = function(widget, handler) {
+    if (ui.isMobile()) {
+        widget.ontouchstart = function(e) {
+            return ui.ontouchstart(widget, e);
+        };
+        widget.ontouchmove = function(e) {
+            return ui.ontouchmove(widget, e);
+        };
+        widget.ontouchend = function(e) {
+            return ui.ontouchend(widget, e);
+        };
+    }
+    widget.onclick = function(e) {
+        //debug('onclick');
+        return handler(e);
+    };
+    return widget;
+};
+
+/**
  * Build a portable <a href> tag.
  */
 ui.href = function(id, loc, target, html) {
     if (target == '_blank')
         return '<a id="' + id + '" href="' + loc + '" target="_blank">' + html + '</a>';
-    return '<a id="' + id + '" href="' + loc + '" onclick="return ui.navigate(\'' + loc + '\', \'' + target + '\');">' + html + '</a>';
+    return '<a id="' + id + '" href="' + loc + '" ' + (ui.isMobile()? 'ontouchstart="return ui.ontouchstart(this, event);" ontouchmove="return ui.ontouchmove(this, event);" ontouchend="return ui.ontouchend(this, event);" ' : '') + 'onclick="return ui.navigate(\'' + loc + '\', \'' + target + '\');">' + html + '</a>';
 };
 
 /**
@@ -489,7 +571,7 @@
     function Menu() {
         this.content = function() {
             function href(id, fun, html) {
-                return '<a id="' + id + '" href="/" onclick="' + fun + '">' + html + '</a>';
+                return '<a id="' + id + '" href="/" ' + (ui.isMobile()? 'ontouchstart="return ui.ontouchstart(this, event);" ontouchmove="return ui.ontouchmove(this, event);" ontouchend="return ui.ontouchend(this, event);" ' : '') + 'onclick="' + fun + '">' + html + '</a>';
             }
 
             if (hilight == true)