This closes #121
diff --git a/modularity-server/metadata-registry/src/main/resources/OSGI-INF/blueprint/blueprint.xml b/modularity-server/metadata-registry/src/main/resources/OSGI-INF/blueprint/blueprint.xml
index ca92875..3c0293a 100644
--- a/modularity-server/metadata-registry/src/main/resources/OSGI-INF/blueprint/blueprint.xml
+++ b/modularity-server/metadata-registry/src/main/resources/OSGI-INF/blueprint/blueprint.xml
@@ -34,7 +34,36 @@
         </cm:default-properties>
     </cm:property-placeholder>
 
+    <reference id="localManagementContext"
+               interface="org.apache.brooklyn.core.mgmt.internal.ManagementContextInternal"/>
 
+    <reference id="shutdownHandler" interface="org.apache.brooklyn.core.mgmt.ShutdownHandler"/>
+
+    <cm:property-placeholder persistent-id="org.apache.brooklyn.rest.filter.cors" placeholder-prefix="$cors{">
+        <!-- since we have properties above for this bundle we need a unique prefix/suffix here -->
+        <cm:default-properties>
+            <cm:property name="cors.enabled" value="true"/>
+            <cm:property name="cors.allow.origins" value=""/>
+            <cm:property name="cors.allow.headers" value=""/>
+            <cm:property name="cors.expose.headers" value=""/>
+            <cm:property name="cors.allow.credentials" value="true"/>
+            <cm:property name="cors.max.age" value="-1"/>
+            <cm:property name="cors.preflight.error.status" value="200"/>
+            <cm:property name="cors.block.if.unauthorized" value="false"/>
+        </cm:default-properties>
+    </cm:property-placeholder>
+    
+    <bean class="org.apache.brooklyn.rest.filter.CorsImplSupplierFilter" id="cors-filter">
+        <property name="enableCors" value="$cors{cors.enabled}"/>
+        <property name="allowOrigins" value="$cors{cors.allow.origins}"/>
+        <property name="allowHeaders" value="$cors{cors.allow.headers}"/>
+        <property name="exposeHeaders" value="$cors{cors.expose.headers}"/>
+        <property name="allowCredentials" value="$cors{cors.allow.credentials}"/>
+        <property name="maxAge" value="$cors{cors.max.age}"/>
+        <property name="preflightErrorStatus" value="$cors{cors.preflight.error.status}"/>
+        <property name="blockCorsIfUnauthorized" value="$cors{cors.block.if.unauthorized}"/>
+    </bean>
+    
     <bean id="registry" class="org.apache.brooklyn.ui.modularity.metadata.registry.impl.UiMetadataRegistryImpl"/>
 
     <service id="uiMetadataRegistryService" ref="registry" interface="org.apache.brooklyn.ui.modularity.metadata.registry.UiMetadataRegistry"/>
@@ -45,14 +74,30 @@
                 <property name="metadataRegistry" ref="registry"/>
             </bean>
         </jaxrs:serviceBeans>
-        <jaxrs:inInterceptors>
-            <bean class="org.apache.cxf.interceptor.LoggingInInterceptor"/>
-        </jaxrs:inInterceptors>
-        <jaxrs:outInterceptors>
-            <bean class="org.apache.cxf.interceptor.LoggingOutInterceptor"/>
-        </jaxrs:outInterceptors>
+        
         <jaxrs:providers>
-            <bean class="com.fasterxml.jackson.jaxrs.json.JacksonJsonProvider"/>
+            <bean class="org.apache.brooklyn.rest.util.DefaultExceptionMapper"/>
+            <bean class="org.apache.brooklyn.rest.util.json.BrooklynJacksonJsonProvider"/>
+            <bean class="org.apache.brooklyn.rest.util.FormMapProvider"/>
+            <bean class="org.apache.brooklyn.rest.util.ManagementContextProvider">
+                <argument ref="localManagementContext"/>
+            </bean>
+            <bean class="org.apache.brooklyn.rest.filter.BrooklynSecurityProviderFilterJersey"/>
+            <bean class="org.apache.brooklyn.rest.filter.CsrfTokenFilter"/>
+            <bean class="org.apache.brooklyn.rest.filter.RequestTaggingRsFilter"/>
+            <bean class="org.apache.brooklyn.rest.filter.NoCacheFilter"/>
+            <bean class="org.apache.brooklyn.rest.filter.HaHotCheckResourceFilter"/>
+            <bean class="org.apache.brooklyn.rest.filter.EntitlementContextFilter"/>
+            <bean class="org.apache.brooklyn.rest.filter.LoggingResourceFilter"/>
+            <bean class="io.swagger.jaxrs.listing.SwaggerSerializers"/>
+            <bean class="org.apache.brooklyn.rest.util.ShutdownHandlerProvider">
+                <argument ref="shutdownHandler"/>
+            </bean>
+            <ref component-id="cors-filter"/>
         </jaxrs:providers>
+
+        <jaxrs:properties>
+            <entry key="default.wae.mapper.least.specific" value="true"/>
+        </jaxrs:properties>
     </jaxrs:server>
 </blueprint>
diff --git a/modularity-server/module-api/src/main/java/org/apache/brooklyn/ui/modularity/module/api/UiModule.java b/modularity-server/module-api/src/main/java/org/apache/brooklyn/ui/modularity/module/api/UiModule.java
index 43bdeb2..e50d75b 100644
--- a/modularity-server/module-api/src/main/java/org/apache/brooklyn/ui/modularity/module/api/UiModule.java
+++ b/modularity-server/module-api/src/main/java/org/apache/brooklyn/ui/modularity/module/api/UiModule.java
@@ -21,6 +21,8 @@
 import java.util.List;
 import java.util.Set;
 
+import org.apache.brooklyn.ui.modularity.module.api.internal.UiModuleImpl;
+
 public interface UiModule {
     String DEFAULT_ICON = "fa-cogs";
 
@@ -69,4 +71,10 @@
      * @return Registered module actions
      */
     List<UiModuleAction> getActions();
+    
+    public class Utils {
+        public static UiModule copyUiModule(UiModule src) {
+            return UiModuleImpl.copyOf(src);
+        }
+    }
 }
diff --git a/modularity-server/module-api/src/main/java/org/apache/brooklyn/ui/modularity/module/api/internal/UiModuleImpl.java b/modularity-server/module-api/src/main/java/org/apache/brooklyn/ui/modularity/module/api/internal/UiModuleImpl.java
index 845cc8a..7d62fb1 100644
--- a/modularity-server/module-api/src/main/java/org/apache/brooklyn/ui/modularity/module/api/internal/UiModuleImpl.java
+++ b/modularity-server/module-api/src/main/java/org/apache/brooklyn/ui/modularity/module/api/internal/UiModuleImpl.java
@@ -41,6 +41,20 @@
     private String path;
     private List<UiModuleAction> actions = new ArrayList<>();
 
+    public static UiModuleImpl copyOf(UiModule src) {
+        final UiModuleImpl result = new UiModuleImpl();
+        result.setId(src.getId());
+        result.setName(src.getName());
+        result.setSlug(src.getSlug());
+        result.setIcon(src.getIcon());
+        if (src.getTypes()!=null) result.types.addAll(src.getTypes());
+        if (src.getSupersedesBundles()!=null) result.supersedesBundles.addAll(src.getSupersedesBundles());
+        result.setStopExisting(src.getStopExisting());
+        result.setPath(src.getPath());
+        if (src.getActions()!=null) result.actions.addAll(src.getActions());
+        return result;
+    }
+    
     public static UiModuleImpl createFromMap(final Map<String, ?> incomingMap) {
         final UiModuleImpl result = new UiModuleImpl();
         result.setId(Optional.fromNullable((String) incomingMap.get("id")).or(UUID.randomUUID().toString()));
diff --git a/modularity-server/module-registry/src/main/java/org/apache/brooklyn/ui/modularity/module/registry/RestUiModuleRegistry.java b/modularity-server/module-registry/src/main/java/org/apache/brooklyn/ui/modularity/module/registry/RestUiModuleRegistry.java
index 5aca5b5..86a2350 100644
--- a/modularity-server/module-registry/src/main/java/org/apache/brooklyn/ui/modularity/module/registry/RestUiModuleRegistry.java
+++ b/modularity-server/module-registry/src/main/java/org/apache/brooklyn/ui/modularity/module/registry/RestUiModuleRegistry.java
@@ -18,17 +18,20 @@
  */
 package org.apache.brooklyn.ui.modularity.module.registry;
 
-import com.google.common.base.Function;
-import com.google.common.collect.Ordering;
-import org.apache.brooklyn.ui.modularity.module.api.UiModule;
-import org.apache.brooklyn.ui.modularity.module.api.UiModuleRegistry;
+import java.util.Collection;
 
 import javax.ws.rs.Consumes;
 import javax.ws.rs.GET;
 import javax.ws.rs.Path;
 import javax.ws.rs.Produces;
 import javax.ws.rs.core.MediaType;
-import java.util.Collection;
+
+import org.apache.brooklyn.ui.modularity.module.api.UiModule;
+import org.apache.brooklyn.ui.modularity.module.api.UiModuleRegistry;
+
+import com.google.common.base.Function;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Ordering;
 
 @Path("/")
 public class RestUiModuleRegistry {
@@ -50,7 +53,9 @@
     public Collection<UiModule> getRegisteredWebComponents() {
         return Ordering.natural()
                 .onResultOf(GET_NAME_FUNCTION)
-                .immutableSortedCopy(uiModuleRegistry.getRegisteredModules());
+                .immutableSortedCopy(
+                    // turn it from a proxy to a serializable bean
+                    Iterables.transform(uiModuleRegistry.getRegisteredModules(), x -> UiModule.Utils.copyUiModule(x)));
     }
 
     public void setUiModuleRegistry(final UiModuleRegistry uiModuleRegistry) {
diff --git a/modularity-server/module-registry/src/main/resources/OSGI-INF/blueprint/blueprint.xml b/modularity-server/module-registry/src/main/resources/OSGI-INF/blueprint/blueprint.xml
index 9d718a1..9cd0351 100644
--- a/modularity-server/module-registry/src/main/resources/OSGI-INF/blueprint/blueprint.xml
+++ b/modularity-server/module-registry/src/main/resources/OSGI-INF/blueprint/blueprint.xml
@@ -36,7 +36,6 @@
         </cm:default-properties>
     </cm:property-placeholder>
 
-
     <bean id="servlet" class="org.apache.brooklyn.ui.modularity.module.registry.internal.RedirectServlet">
         <argument index="0" type="java.lang.String" value="${redirect.path}"/>
     </bean>
@@ -46,6 +45,36 @@
         </service-properties>
     </service>
 
+    <reference id="localManagementContext"
+               interface="org.apache.brooklyn.core.mgmt.internal.ManagementContextInternal"/>
+
+    <reference id="shutdownHandler" interface="org.apache.brooklyn.core.mgmt.ShutdownHandler"/>
+
+    <cm:property-placeholder persistent-id="org.apache.brooklyn.rest.filter.cors" placeholder-prefix="$cors{">
+        <!-- since we have properties above for this bundle we need a unique prefix/suffix here -->
+        <cm:default-properties>
+            <cm:property name="cors.enabled" value="true"/>
+            <cm:property name="cors.allow.origins" value=""/>
+            <cm:property name="cors.allow.headers" value=""/>
+            <cm:property name="cors.expose.headers" value=""/>
+            <cm:property name="cors.allow.credentials" value="true"/>
+            <cm:property name="cors.max.age" value="-1"/>
+            <cm:property name="cors.preflight.error.status" value="200"/>
+            <cm:property name="cors.block.if.unauthorized" value="false"/>
+        </cm:default-properties>
+    </cm:property-placeholder>
+    
+    <bean class="org.apache.brooklyn.rest.filter.CorsImplSupplierFilter" id="cors-filter">
+        <property name="enableCors" value="$cors{cors.enabled}"/>
+        <property name="allowOrigins" value="$cors{cors.allow.origins}"/>
+        <property name="allowHeaders" value="$cors{cors.allow.headers}"/>
+        <property name="exposeHeaders" value="$cors{cors.expose.headers}"/>
+        <property name="allowCredentials" value="$cors{cors.allow.credentials}"/>
+        <property name="maxAge" value="$cors{cors.max.age}"/>
+        <property name="preflightErrorStatus" value="$cors{cors.preflight.error.status}"/>
+        <property name="blockCorsIfUnauthorized" value="$cors{cors.block.if.unauthorized}"/>
+    </bean>
+
     <bean id="module-registry"
           class="org.apache.brooklyn.ui.modularity.module.registry.UiModuleRegistryImpl"/>
     <reference-list interface="org.apache.brooklyn.ui.modularity.module.api.UiModule" availability="optional">
@@ -54,20 +83,36 @@
 
     <service interface="org.apache.brooklyn.ui.modularity.module.api.UiModuleRegistry" ref="module-registry"/>
 
-    <jaxrs:server id="brooklynRestApiV1" address="${ui.module.api.path}">
+    <jaxrs:server id="brooklynRestApiV1UiModuleRegistry" address="${ui.module.api.path}">
         <jaxrs:serviceBeans>
             <bean class="org.apache.brooklyn.ui.modularity.module.registry.RestUiModuleRegistry">
                 <property name="uiModuleRegistry" ref="module-registry"/>
             </bean>
         </jaxrs:serviceBeans>
-        <jaxrs:inInterceptors>
-            <bean class="org.apache.cxf.interceptor.LoggingInInterceptor"/>
-        </jaxrs:inInterceptors>
-        <jaxrs:outInterceptors>
-            <bean class="org.apache.cxf.interceptor.LoggingOutInterceptor"/>
-        </jaxrs:outInterceptors>
+        
         <jaxrs:providers>
-            <bean class="com.fasterxml.jackson.jaxrs.json.JacksonJsonProvider"/>
+            <bean class="org.apache.brooklyn.rest.util.DefaultExceptionMapper"/>
+            <bean class="org.apache.brooklyn.rest.util.json.BrooklynJacksonJsonProvider"/>
+            <bean class="org.apache.brooklyn.rest.util.FormMapProvider"/>
+            <bean class="org.apache.brooklyn.rest.util.ManagementContextProvider">
+                <argument ref="localManagementContext"/>
+            </bean>
+            <bean class="org.apache.brooklyn.rest.filter.BrooklynSecurityProviderFilterJersey"/>
+            <bean class="org.apache.brooklyn.rest.filter.CsrfTokenFilter"/>
+            <bean class="org.apache.brooklyn.rest.filter.RequestTaggingRsFilter"/>
+            <bean class="org.apache.brooklyn.rest.filter.NoCacheFilter"/>
+            <bean class="org.apache.brooklyn.rest.filter.HaHotCheckResourceFilter"/>
+            <bean class="org.apache.brooklyn.rest.filter.EntitlementContextFilter"/>
+            <bean class="org.apache.brooklyn.rest.filter.LoggingResourceFilter"/>
+            <bean class="io.swagger.jaxrs.listing.SwaggerSerializers"/>
+            <bean class="org.apache.brooklyn.rest.util.ShutdownHandlerProvider">
+                <argument ref="shutdownHandler"/>
+            </bean>
+            <ref component-id="cors-filter"/>
         </jaxrs:providers>
+
+        <jaxrs:properties>
+            <entry key="default.wae.mapper.least.specific" value="true"/>
+        </jaxrs:properties>
     </jaxrs:server>
 </blueprint>
diff --git a/ui-modules/app-inspector/app/views/main/inspect/summary/summary.controller.js b/ui-modules/app-inspector/app/views/main/inspect/summary/summary.controller.js
index aff3f62..5c30829 100644
--- a/ui-modules/app-inspector/app/views/main/inspect/summary/summary.controller.js
+++ b/ui-modules/app-inspector/app/views/main/inspect/summary/summary.controller.js
@@ -150,7 +150,7 @@
         vm.error.policies = 'Cannot load policies for entity with ID: ' + entityId;
     });
 
-    $http.get('v1/ui-metadata-registry', {params: {type: 'location'}}).then(response => {
+    $http.get('/v1/ui-metadata-registry', {params: {type: 'location'}}).then(response => {
         vm.metadata = response.data;
     });
 
diff --git a/ui-modules/logout/Makefile b/ui-modules/logout/Makefile
new file mode 100644
index 0000000..c7cb424
--- /dev/null
+++ b/ui-modules/logout/Makefile
@@ -0,0 +1,49 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+default: dev
+
+build:
+	@echo "Building production bundle..."
+	NODE_ENV="production" npm run build
+
+clean:
+	@echo "Cleaning modules..."
+	@rm -rf ./node_modules
+
+dev:
+	@echo "Starting dev web server..."
+	@npm start
+
+server: build
+	@echo "Starting api proxy server..."
+	NODE_ENV="production" npm start
+
+install:
+	@echo "Installing npm modules..."
+	@npm install
+
+test:
+	@echo "Running tests..."
+	@npm test
+
+setup: clean install
+
+war:
+	@mvn clean install
+
+.PHONY: build clean deploy dev install server setup test war
diff --git a/ui-modules/logout/app/index.js b/ui-modules/logout/app/index.js
index 09f6e4f..e5d4d15 100644
--- a/ui-modules/logout/app/index.js
+++ b/ui-modules/logout/app/index.js
@@ -26,10 +26,11 @@
 import brInterstitialSpinner from 'brooklyn-ui-utils/interstitial-spinner/interstitial-spinner';
 import brooklynModuleLinks from 'brooklyn-ui-utils/module-links/module-links';
 import brooklynUserManagement from 'brooklyn-ui-utils/user-management/user-management';
+import brServerStatus from 'brooklyn-ui-utils/server-status/server-status';
 
 import mainState from './views/main/main.controller';
 
-angular.module('app', [ngAnimate, ngCookies, uiRouter, brCore, brInterstitialSpinner, brooklynModuleLinks, brooklynUserManagement, mainState])
+angular.module('app', [ngAnimate, ngCookies, uiRouter, brCore, brInterstitialSpinner, brServerStatus, brooklynModuleLinks, brooklynUserManagement, mainState])
     .config(['$urlRouterProvider', '$logProvider', applicationConfig])
     .run(['$http', httpConfig]);
 
diff --git a/ui-modules/logout/app/index.less b/ui-modules/logout/app/index.less
index 5750772..1f64564 100644
--- a/ui-modules/logout/app/index.less
+++ b/ui-modules/logout/app/index.less
@@ -19,7 +19,7 @@
 @import '~brooklyn-shared/style/first.less';
 
 // Add project less files here
-
+.text-narrow > div { margin-left: 25%; margin-right: 25%; }
 
 // Load last so that these style rules and var values trump others
 @import "~brooklyn-shared/style/last.less";
diff --git a/ui-modules/logout/app/views/main/main.controller.js b/ui-modules/logout/app/views/main/main.controller.js
index 8e554fb..7fda72f 100644
--- a/ui-modules/logout/app/views/main/main.controller.js
+++ b/ui-modules/logout/app/views/main/main.controller.js
@@ -28,51 +28,123 @@
 export default MODULE_NAME;
 
 export const mainState = {
-    name: 'main',
-    url: '/',
+    name: 'mainRoot',
+    url: '/?debug&keepCreds&useGet&salt',
+    // experimental/test options:
+    // * useGet means to make a GET request instead of POST
+    // * keepCreds means not to request a 200 on successful logout instead of a 401;
+    //   this will prevent the browser from clearing cache 
     template: require('ejs-html!./main.template.html'),
-    controller: ['$scope', mainStateController],
+    controller: ['$scope', '$http', '$state', '$stateParams', '$log', '$timeout', mainStateController],
+    controllerAs: 'vm'
+};
+export const promptState = {
+    name: 'prompt',
+    url: '/prompt?debug',
+    params: { prompt: true },
+    template: require('ejs-html!./main.template.html'),
+    controller: ['$scope', '$http', '$state', '$stateParams', mainStateController],
     controllerAs: 'vm'
 };
 
 export function mainStateConfig($stateProvider) {
-    $stateProvider.state(mainState);
+    $stateProvider.state(promptState).state(mainState);
 }
 
-export function mainStateController($scope) {
+export function mainStateController($scope, $http, $state, $stateParams, $log, $timeout) {
+    if (!$scope.state) $scope.state = {};
+    if ($stateParams.prompt) $scope.state.status = "prompt";
+    if (!$scope.state.status) $scope.state.status = "do-logout";
+    
+    /* There is a lot of complexity in here to support debug pathways with confirmation, 
+     * use of http GET instead of POST, and use of API which returns 200 instead of 401.
+     * This is because logging out nicely is quite tricky.
+     *   Currently we think we have a good pathway without any of that complexity,
+     * so if you haven't set "?debug=true" or other special option in the URL it is
+     * mostly disabled and follows the happy path where it just logs out and prompts
+     * you to log back in. But the debug stuff is left in, in case we encounter edge cases.
+     */
+     
+    $scope.debug = $stateParams.debug;
+    if ($scope.debug) {
+        $log.info("Logout page running in debug mode. state=", $state, "state params=", $stateParams);
+    }
+    if ($stateParams.salt) {
+        // specify some salt to ensure links change in dev mode
+        $scope.salt = (parseInt($stateParams.salt) || 0);
+    }
+    
     $scope.$emit(HIDE_INTERSTITIAL_SPINNER_EVENT);
 
-    let userRequest = new XMLHttpRequest();
-    userRequest.onreadystatechange = function () {
-        if (this.readyState === 4 && this.status === 200) {
-            logout(this.responseText)
-        }
-    };
-    userRequest.open('GET', '/v1/server/user', true);
-    userRequest.send('');
-
-    /**
-     * Logout the supplied user
-     * @param user
-     */
-    function logout(user) {
+    function clearLocalCache() {
         let ua = window.navigator.userAgent;
         if (ua.indexOf('MSIE ') >= 0 || ua.indexOf(' Edge/') >= 0 || ua.indexOf(' Trident/') >= 0) {
             document.execCommand('ClearAuthenticationCache', 'false');
         }
-        let logoutRequest = new XMLHttpRequest();
-        logoutRequest.onreadystatechange = function () {
-            if (this.readyState === 4) {
-                if (this.status === 401) {
-                    console.info('User ' + user + ' logged out')
-                } else {
-                    setTimeout(function () {
-                        logout(user);
-                    }, 1000);
-                }
-            }
-        };
-        logoutRequest.open('POST', '/v1/logout', true, user, Math.random().toString(36).slice(2));
-        logoutRequest.send('');
     }
+
+    function handleError(phase, response, expectAlreadyLoggedOut) {
+        if (response && response.status >= 300 && response.status < 400 || response.status == 401) {
+            // auth required
+            if (expectAlreadyLoggedOut) {
+                $scope.state = { status: "logout-confirmed", code: response.status };
+            } else {
+                $scope.state = { status: "already-logged-out", code: response.status };
+            }
+        } else if (response && response.status && response.status>0) {
+            $log.warn("Server failure "+phase, response);
+            $scope.state = { status: "failed", message: "server failure ("+response.status+") "+phase+
+                (response.message ? ": "+response.message : ""), code: response.status };
+        } else {
+            $log.info("Connection failure "+phase, response);
+            $scope.state = { status: "failed", message: "connection failure "+phase, code: response.status };
+        }
+        clearLocalCache();
+    }
+    
+    this.logout = (expectAlreadyLoggedOut) => {
+        let useGet = $stateParams.useGet;
+        let keepCreds = $stateParams.keepCreds;
+        
+        $scope.state = { status: "logging-out" };
+        let params = {};
+        let ourToken = 'logging-out-from-webapp';  // used to ensure the 401 is because we logged out
+        if (!keepCreds) params.unauthorize = ourToken;
+        //let httpCall = useGet ? $http.get('/v1/logout', { params }) : $http.post('/v1/logout', params);
+        //httpCall
+        $timeout(()=>
+          $http({ url: '/v1/logout', method: useGet ? "GET" : "POST", params })
+          .then(response => {
+            if ($scope.debug) $log.info("Logout response", response);
+            $scope.state = { status: "just-logged-out" };
+            clearLocalCache();
+            
+          }, error => {
+            if (error.data && error.data.message == ourToken) {
+                if ($scope.debug) $log.info("Logout response 401 - ", error);
+                if (expectAlreadyLoggedOut) {
+                    $scope.state = { status: "success-after-logout" };
+                } else {
+                    $scope.state = { status: "just-logged-out" };
+                }
+                clearLocalCache();
+                
+            } else {
+                handleError(expectAlreadyLoggedOut ? "confirming logout" : "logging out", error, expectAlreadyLoggedOut);
+            }
+          }), 500 /* delay 500ms so other requests finish loading */);
+    }
+    
+    this.retry = () => this.logout();
+    
+    this.prompt = () => {
+        $scope.state.status = "prompt";
+    }
+    
+    this.confirm = () => {
+        clearLocalCache();
+        this.logout(true);
+    }
+    
+    if ($scope.state.status == "do-logout") this.logout();
 }
diff --git a/ui-modules/logout/app/views/main/main.template.html b/ui-modules/logout/app/views/main/main.template.html
index 752ac93..ba5ff77 100644
--- a/ui-modules/logout/app/views/main/main.template.html
+++ b/ui-modules/logout/app/views/main/main.template.html
@@ -18,9 +18,72 @@
 -->
 <div class="container-fluid">
     <div class="row">
-        <div class="col-md-12 text-center">
-            <h2>Thank you for using <%= getBrandedText('product.name') %>, see you soon</h2>
-            <p><a class="btn btn-lg btn-primary" href="/">Log back in</a></p>
+        <div class="col-md-12 text-center text-narrow" ng-switch="state.status">
+            <div ng-switch-when="prompt">
+                <h2>Are you sure you wish to logout?</h2>
+                <p>
+                    <a class="btn btn-lg btn-primary" ng-click="vm.logout()">Yes</a>
+                    <a class="btn btn-lg btn-outline" href="/">No, return to <%= getBrandedText('product.name') %></a>
+                </p>
+                <div br-server-status></div>
+            </div>
+            
+            <div ng-switch-when="logging-out">
+                <h2>Logging out...</h2>
+                <p>&nbsp;</p>
+                <p><a class="btn btn-lg btn-primary" ng-click="vm.retry()">Retry</a></p>
+            </div>
+            
+            <div ng-switch-when="just-logged-out">
+                <h2>Logout successful</h2>
+                <p>Thank you for using <%= getBrandedText('product.name') %>. See you soon!</p>
+                <p><i>Note that some browsers may cache credentials, 
+                    particularly if you have this app open in multiple tabs
+                    so you might not be prompted on a subsequent log in.</i></p>
+                <p>&nbsp;</p>
+                <p><a class="btn btn-lg btn-primary" href="/{{ debug || salt ? '#!/?' : '' }}{{ debug ? '&debug=true' : ''}}{{ salt ? '&salt='+(salt+1) : ''}}">Log back in</a></p>
+                <p ng-if="debug"><a class="btn btn-lg btn-outline" ng-click="vm.confirm()">Confirm and clear</a></p>
+            </div>
+            
+            <div ng-switch-when="already-logged-out">
+                <h2>Already logged out</h2>
+                <p>Thank you for using <%= getBrandedText('product.name') %>. See you soon!</p>
+                <p>&nbsp;</p>
+                <p><a class="btn btn-lg btn-primary" href="/{{ debug || salt ? '#!/?' : '' }}{{ debug ? '&debug=true' : ''}}{{ salt ? '&salt='+(salt+1) : ''}}">Log back in</a></p>
+                <p ng-if="debug"><a class="btn btn-lg btn-outline" ng-click="vm.confirm()">Confirm and clear</a></p>
+            </div>
+            
+            <div ng-switch-when="logout-confirmed">
+                <h2>Logout confirmed</h2>
+                <p>Thank you for using <%= getBrandedText('product.name') %>. See you soon!</p>
+                <p>The application is no longer able to log in to the server.</p>
+                <p>&nbsp;</p>
+                <p><a class="btn btn-lg btn-primary" href="/{{ debug || salt ? '#!/?' : '' }}{{ debug ? '&debug=true' : ''}}{{ salt ? '&salt='+(salt+1) : ''}}">Log back in</a></p>
+                <p ng-if="debug"><a class="btn btn-lg btn-outline" ng-click="vm.confirm()">Confirm and clear again</a></p>
+            </div>
+            <div ng-switch-when="success-after-logout">
+                <h2>Logged out again</h2>
+                <p>Thank you for using <%= getBrandedText('product.name') %>. See you soon!</p>
+                
+                <p>The app was able to log in successfully and logout again.</p>
+                <p>If you did not log again, this suggests the browser may be caching credentials or the server is unsecured.</p> 
+                <p>The server has confirmed it has deleted the session.</p>
+                <p>&nbsp;</p>
+                <p><a class="btn btn-lg btn-primary" href="/{{ debug || salt ? '#!/?' : '' }}{{ debug ? '&debug=true' : ''}}{{ salt ? '&salt='+(salt+1) : ''}}">Log back in</a></p>
+                <p ng-if="debug"><a class="btn btn-lg btn-outline" ng-click="vm.confirm()">Confirm and clear</a></p>
+            </div>
+            
+            
+            <div ng-switch-default>
+                <h2>Logout failed<span ng-if="state.message">: {{ state.message }}</span></h2>
+                <p ng-if="state.code==403"><i>CSRF cookie errors are common in development or can indicate that multiple sessions are open.
+                    Closing other tabs and retrying will usually fix this.</i></p>
+                <p><i>More information may be available in the logs.</i></p>
+                <p>&nbsp;</p>
+                <p><a class="btn btn-lg btn-primary" ng-click="vm.retry()">Retry</a></p>
+                <p ng-if="debug"><a class="btn btn-lg btn-outline" ui-sref="/prompt">Prompt to logout again</a></p>
+                <p><a class="btn btn-lg btn-outline" href="/">Return to <%= getBrandedText('product.name') %> (login if needed)</a></p>
+            </div>
         </div>
     </div>
 </div>
diff --git a/ui-modules/logout/src/main/webapp/WEB-INF/web.xml b/ui-modules/logout/src/main/webapp/WEB-INF/web.xml
index 0355330..5955b71 100644
--- a/ui-modules/logout/src/main/webapp/WEB-INF/web.xml
+++ b/ui-modules/logout/src/main/webapp/WEB-INF/web.xml
@@ -28,6 +28,46 @@
         <welcome-file>index.html</welcome-file>
     </welcome-file-list>
 
-    <!--no security for this module so we can confirm that the user has logged out-->
+<!-- NOT registered as a brooklyn ui module; don't want it to be listed as a module
+    <listener>
+        <listener-class>org.apache.brooklyn.ui.modularity.module.api.UiModuleListener</listener-class>
+    </listener>
+-->
+
+    <!--FILTERS :: START-->
+    <filter>
+        <filter-name>ui-module-filter</filter-name>
+        <filter-class>org.apache.brooklyn.ui.modularity.module.api.UiModuleFilter</filter-class>
+        <init-param>
+            <param-name>Cache-Control</param-name>
+            <param-value>public, max-age=604800</param-value><!--Cache static content for 1 week-->
+        </init-param>
+    </filter>
+    <filter>
+        <filter-name>brooklyn-security-filter</filter-name>
+        <filter-class>org.apache.brooklyn.rest.filter.BrooklynSecurityProviderFilterJavax</filter-class>
+    </filter>
+    <filter>
+        <filter-name>GzipFilter</filter-name>
+        <filter-class>org.eclipse.jetty.servlets.GzipFilter</filter-class>
+        <init-param>
+            <param-name>mimeTypes</param-name>
+            <param-value>text/html,text/plain,text/xml,application/xhtml+xml,text/css,application/javascript,image/svg+xml</param-value>
+        </init-param>
+    </filter>
+
+    <filter-mapping>
+        <filter-name>ui-module-filter</filter-name>
+        <url-pattern>/*</url-pattern>
+    </filter-mapping>
+    <filter-mapping>
+        <filter-name>brooklyn-security-filter</filter-name>
+        <url-pattern>/*</url-pattern>
+    </filter-mapping>
+    <filter-mapping>
+        <filter-name>GzipFilter</filter-name>
+        <url-pattern>/*</url-pattern>
+    </filter-mapping>
+    <!--FILTERS :: END-->
 
 </web-app>