Implemented WebSockets for GUI2

Change-Id: I4776ce392b1e8e94ebee938cf7df22791a1e0b8f
diff --git a/web/gui2/AngularMigration.md b/web/gui2/AngularMigration.md
index b40db34..bb83d18 100644
--- a/web/gui2/AngularMigration.md
+++ b/web/gui2/AngularMigration.md
@@ -5,6 +5,7 @@
 * In tsconfig.json this app is set to be compiled as ES6 (ES2015)
   * See this [Compatibility Table](http://kangax.github.io/compat-table/es6/) for supported browsers
   * All modern browsers are supported
+  * See https://webapplog.com/es6/ for a list of things that ES6 brings
 * Each item (Service, Component, Directive, Pipe or Module) gets its own file ending with this type e.g. function.service.ts 
 * Each test file is the name of the item with .spec.ts e.g. function.service.spec.ts
 * Modules are used to group together services, components, directives etc
@@ -83,11 +84,59 @@
 * LogService - this replaces $log that was inserted in to the old code
 * WindowService - this replaces $window and $location in the old code
 
+There is a fair bit of refactoring has to take place. An important thing to understand
+is that DOM manipulations from inside JavaScript code is not the Angular 6
+way of doing things - there was a lot of this in the old ONOS GUI, using d3.append(..)
+and so on.
+The Angular 6 way of doing things is to defined DOM objects (elements) in the 
+html template of a component, and use the Component Java Script code as a base
+for logic that can influence the display of these objects in the template.
+What this means is that what were previously defined as services (e.g. VeilService or
+LoadingService) should now become Components in Angular 6 (e.g. VeilComponent or
+LoadingComponent). 
+
+###How do I know whether a Service should be made a Component in this new GUI?
+The general rule to follow is _"if a service in the old GUI has an associated CSS 
+file or two then is should be a component in the new GUI"_. 
+
+The implication of this is that all of the d3 DOM Manipulations that happened in 
+the old service should now be represented in the template of this new component.
+If it's not clear to you what the template should look like, then run the old GUI
+and inspect the element and its children to see the structure.
+
+Components (unlike services) have limited scope (that's the magic of them really -
+no more DOM is loaded at any time than is necessary). This means that they are
+self contained modules, and any CSS associated with them is private to that 
+component and not accessible globally.
+
+### Do not inject components in to services
+Components are graphical elements and should not be injected in to Services. 
+Services should be injected in to components, but not the other way round.
+
+Take for instance the WebSocketService - this should remain a service, but I want 
+to display the LoadingComponent while it's waiting and the VeilComponent if it
+disconnects. I should not go injecting these in to WebSocketService - instead
+there is a setLoadingDelegate() and a setVeilDelegate() function on WSS that I 
+can pass in a reference to these two components. When they need to be displayed
+a method call is made on the delegate and the component gets enabled and displays.
+Also note inside WSS any time we call a method on this LoadingComponent delegate 
+we check that it the delegate had actually been set. 
+
+The WSS was passed in to the LoadingComponent and VeilComponent to set the 
+delegate on it.
+
+Any component that needs to use WSS for data should inject the WSS service __AND__
+needs to include the components in its template by adding <onos-loading> and
+<onos-veil>.
+
 ## fw/remote/wsock.service
 Taking for a really simple example the fw/remote/WSockService, this was originally defined in 
 the __app/fw/remote/wsock.js__ file and is now redefined in 
 __onos/web/gui2/src/main/webapp/app/fw/remote/wsock.service.ts__.
 
+First of all this should remain a Service, since it does not do any DOM
+manipulation and does not have an associated CSS.
+
 This has one method that's called to establish the WebSocketService
 
 ```javascript
diff --git a/web/gui2/angular.json b/web/gui2/angular.json
index 887eaa4..8341e91 100644
--- a/web/gui2/angular.json
+++ b/web/gui2/angular.json
@@ -20,9 +20,7 @@
               "src/main/webapp/data",
               "src/main/webapp/app/fw/layer/loading.service.css"
             ],
-            "styles": [
-              "src/main/webapp/app/onos.css"
-            ],
+            "styles": ["src/main/webapp/onos.theme.css"],
             "scripts": []
           },
           "configurations": {
@@ -71,7 +69,7 @@
             "tsConfig": "src/main/webapp/tsconfig.spec.json",
             "scripts": [],
             "styles": [
-              "src/main/webapp/app/onos.css"
+              "src/main/webapp/app/onos.theme.css"
             ],
             "assets": [
               "src/main/webapp/data",
diff --git a/web/gui2/src/main/webapp/app/detectbrowser.directive.ts b/web/gui2/src/main/webapp/app/detectbrowser.directive.ts
index b69ca39..b8a3c77 100644
--- a/web/gui2/src/main/webapp/app/detectbrowser.directive.ts
+++ b/web/gui2/src/main/webapp/app/detectbrowser.directive.ts
@@ -13,6 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+import { Inject } from '@angular/core';
 import { Directive } from '@angular/core';
 import { FnService } from './fw/util/fn.service';
 import { LogService } from './log.service';
@@ -28,10 +29,9 @@
   constructor(
     private fs: FnService,
     private log: LogService,
-    private onos: OnosService
+    private onos: OnosService,
+    @Inject(Window) private w: Window
   ) {
-        log.debug('DetectBrowserDirective constructed');
-
         const body: HTMLBodyElement = document.getElementsByTagName('body')[0];
 //        let body = d3.select('body');
         let browser = '';
@@ -44,7 +44,10 @@
         } else if (fs.isFirefox()) {
             browser = 'firefox';
         } else {
-            this.log.warn('Unknown browser:', window.navigator.vendor);
+            this.log.warn('Unknown browser. ',
+            'Vendor:', this.w.navigator.vendor,
+            'Agent:', this.w.navigator.userAgent);
+            return;
         }
         body.classList.add(browser);
 //        body.classed(browser, true);
diff --git a/web/gui2/src/main/webapp/app/fw/layer/layer.module.ts b/web/gui2/src/main/webapp/app/fw/layer/layer.module.ts
index e01b004..6292a8d 100644
--- a/web/gui2/src/main/webapp/app/fw/layer/layer.module.ts
+++ b/web/gui2/src/main/webapp/app/fw/layer/layer.module.ts
@@ -24,21 +24,23 @@
 import { LoadingService } from './loading.service';
 import { PanelService } from './panel.service';
 import { QuickHelpService } from './quickhelp.service';
-import { VeilService } from './veil.service';
+import { VeilComponent } from './veil/veil.component';
 
 /**
  * ONOS GUI -- Layers Module
  */
 @NgModule({
   exports: [
-    FlashComponent
+    FlashComponent,
+    VeilComponent
   ],
   imports: [
     CommonModule,
     UtilModule
   ],
   declarations: [
-    FlashComponent
+    FlashComponent,
+    VeilComponent
   ],
   providers: [
     DetailsPanelService,
@@ -46,8 +48,7 @@
     EditableTextService,
     LoadingService,
     PanelService,
-    QuickHelpService,
-    VeilService
+    QuickHelpService
   ]
 })
 export class LayerModule { }
diff --git a/web/gui2/src/main/webapp/app/fw/layer/veil.service.ts b/web/gui2/src/main/webapp/app/fw/layer/veil.service.ts
deleted file mode 100644
index d755e5d..0000000
--- a/web/gui2/src/main/webapp/app/fw/layer/veil.service.ts
+++ /dev/null
@@ -1,42 +0,0 @@
-/*
- * Copyright 2015-present Open Networking Foundation
- *
- * Licensed 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.
- */
-import { Injectable } from '@angular/core';
-import { FnService } from '../util/fn.service';
-import { GlyphService } from '../svg/glyph.service';
-import { KeyService } from '../util/key.service';
-import { LogService } from '../../log.service';
-import { WebSocketService } from '../remote/websocket.service';
-
-/**
- * ONOS GUI -- Layer -- Veil Service
- *
- * Provides a mechanism to display an overlaying div with information.
- * Used mainly for web socket connection interruption.
- */
-@Injectable()
-export class VeilService {
-
-  constructor(
-    private fs: FnService,
-    private gs: GlyphService,
-    private ks: KeyService,
-    private log: LogService,
-    private wss: WebSocketService
-  ) {
-      this.log.debug('VeilService constructed');
-  }
-
-}
diff --git a/web/gui2/src/main/webapp/app/fw/layer/veil/veil.component.css b/web/gui2/src/main/webapp/app/fw/layer/veil/veil.component.css
new file mode 100644
index 0000000..851d6e3
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/fw/layer/veil/veil.component.css
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2015-present Open Networking Foundation
+ *
+ * Licensed 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.
+ */
+
+/*
+ ONOS GUI -- Veil Service (layout) -- CSS file
+ */
+
+#veil {
+    z-index: 5000;
+    display: block;
+    position: absolute;
+    top: 0;
+    left: 0;
+    padding: 60px;
+}
+
+#veil p {
+    display: block;
+    text-align: left;
+    font-size: 14pt;
+    font-style: italic;
+}
diff --git a/web/gui2/src/main/webapp/app/fw/layer/veil/veil.component.html b/web/gui2/src/main/webapp/app/fw/layer/veil/veil.component.html
new file mode 100644
index 0000000..79d202b
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/fw/layer/veil/veil.component.html
@@ -0,0 +1,9 @@
+<div id="veil" *ngIf="enabled">
+    <p *ngFor="let msg of messages">{{ msg }}</p>
+    <svg [attr.width]="fs.windowSize().width" [attr.height]="fs.windowSize().height">
+        <use [attr.width]="birdDim" [attr.height]="birdDim" class="glyph"
+             style="opacity: 0.2;"
+             xlink:href = "#bird" [attr.transform]="trans"/>
+
+    </svg>
+</div>
\ No newline at end of file
diff --git a/web/gui2/src/main/webapp/app/fw/layer/veil/veil.component.spec.ts b/web/gui2/src/main/webapp/app/fw/layer/veil/veil.component.spec.ts
new file mode 100644
index 0000000..0ed493f
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/fw/layer/veil/veil.component.spec.ts
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2015-present Open Networking Foundation
+ *
+ * Licensed 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.
+ */
+
+/*
+ ONOS GUI -- Layer -- Veil Service - Unit Tests
+ */
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { VeilComponent } from './veil.component';
+import { ConsoleLoggerService } from '../../../consolelogger.service';
+import { LogService } from '../../../log.service';
+import { KeyService } from '../../util/key.service';
+import { GlyphService } from '../../svg/glyph.service';
+
+class MockKeyService {}
+
+class MockGlyphService {}
+
+describe('VeilComponent', () => {
+    let log: LogService;
+
+    beforeEach(() => {
+        log = new ConsoleLoggerService();
+
+        TestBed.configureTestingModule({
+            declarations: [ VeilComponent ],
+            providers: [
+                { provide: LogService, useValue: log },
+                { provide: KeyService, useClass: MockKeyService },
+                { provide: GlyphService, useClass: MockGlyphService },
+            ]
+        });
+    });
+
+    it('should create', () => {
+        const fixture = TestBed.createComponent(VeilComponent);
+        const component = fixture.componentInstance;
+        expect(component).toBeTruthy();
+    });
+});
diff --git a/web/gui2/src/main/webapp/app/fw/layer/veil/veil.component.theme.css b/web/gui2/src/main/webapp/app/fw/layer/veil/veil.component.theme.css
new file mode 100644
index 0000000..7a3bded
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/fw/layer/veil/veil.component.theme.css
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2016-present Open Networking Foundation
+ *
+ * Licensed 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.
+ */
+
+/*
+ ONOS GUI -- Veil Service (theme) -- CSS file
+ */
+
+.light, #veil {
+    background-color: rgba(0,0,0,0.75);
+}
+.dark, #veil {
+    background-color: rgba(64,64,64,0.75);
+}
+
+#veil p {
+    color: #ddd;
+}
+
+#veil svg .glyph {
+    fill: #222;
+}
diff --git a/web/gui2/src/main/webapp/app/fw/layer/veil/veil.component.ts b/web/gui2/src/main/webapp/app/fw/layer/veil/veil.component.ts
new file mode 100644
index 0000000..cea6bed
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/fw/layer/veil/veil.component.ts
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2015-present Open Networking Foundation
+ *
+ * Licensed 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.
+ */
+import { Component, OnInit } from '@angular/core';
+import { FnService } from '../../util/fn.service';
+import { GlyphService } from '../../svg/glyph.service';
+import { KeyService } from '../../util/key.service';
+import { LogService } from '../../../log.service';
+import { SvgUtilService } from '../../svg/svgutil.service';
+import { WebSocketService } from '../../remote/websocket.service';
+
+const BIRD = 'bird';
+
+/**
+ * ONOS GUI -- Layer -- Veil Component
+ *
+ * Provides a mechanism to display an overlaying div with information.
+ * Used mainly for web socket connection interruption.
+ *
+ * It can be added to an component's template as follows:
+ *     <onos-veil #veil></onos-veil>
+ *     <p (click)="veil.show(['t1','t2','t3'])">Test Veil</p>
+ */
+@Component({
+  selector: 'onos-veil',
+  templateUrl: './veil.component.html',
+  styleUrls: ['./veil.component.css', './veil.component.theme.css']
+})
+export class VeilComponent implements OnInit {
+    ww: number;
+    wh: number;
+    birdSvg: string;
+    birdDim: number;
+    enabled: boolean = false;
+    trans: string;
+    messages: string[] = [];
+    veilStyle: string;
+
+    constructor(
+        private fs: FnService,
+        private gs: GlyphService,
+        private ks: KeyService,
+        private log: LogService,
+        private sus: SvgUtilService,
+        private wss: WebSocketService
+    ) {
+        const wSize = this.fs.windowSize();
+        this.ww = wSize.width;
+        this.wh = wSize.height;
+        const shrink = this.wh * 0.3;
+        this.birdDim = this.wh - shrink;
+        const birdCenter = (this.ww - this.birdDim) / 2;
+        this.trans = this.sus.translate([birdCenter, shrink / 2]);
+
+        this.log.debug('VeilComponent with ' + BIRD + ' constructed');
+    }
+
+    ngOnInit() {
+    }
+
+    // msg should be an array of strings
+    show(msgs: string[]): void {
+        this.messages = msgs;
+        this.enabled = true;
+//        this.ks.enableKeys(false);
+    }
+
+    hide(): void {
+        this.veilStyle = 'display: none';
+//        this.ks.enableKeys(true);
+    }
+
+
+}
diff --git a/web/gui2/src/main/webapp/app/fw/remote/urlfn.service.ts b/web/gui2/src/main/webapp/app/fw/remote/urlfn.service.ts
index 4685a65..381a0c9 100644
--- a/web/gui2/src/main/webapp/app/fw/remote/urlfn.service.ts
+++ b/web/gui2/src/main/webapp/app/fw/remote/urlfn.service.ts
@@ -13,27 +13,29 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import { Injectable } from '@angular/core';
+import { Injectable, Inject } from '@angular/core';
 import { LogService } from '../../log.service';
 
-const uiContext = '/onos/ui/';
-const rsSuffix = uiContext + 'rs/';
-const wsSuffix = uiContext + 'websock/';
+const UICONTEXT = '/onos/ui/';
+const RSSUFFIX = UICONTEXT + 'rs/';
+const WSSUFFIX = UICONTEXT + 'websock/';
 
 /**
  * ONOS GUI -- Remote -- General Purpose URL Functions
  */
-@Injectable()
+@Injectable({
+  providedIn: 'root',
+})
 export class UrlFnService {
     constructor(
         private log: LogService,
-        private window: Window
+        @Inject(Window) private w: Window
     ) {
         this.log.debug('UrlFnService constructed');
     }
 
     matchSecure(protocol: string): string {
-        const p: string = window.location.protocol;
+        const p: string = this.w.location.protocol;
         const secure: boolean = (p === 'https' || p === 'wss');
         return secure ? protocol + 's' : protocol;
     }
@@ -42,32 +44,35 @@
      * behind a proxy and has an app prefix, e.g.
      *      http://host:port/my/app/onos/ui...
      * This bit of regex grabs everything after the host:port and
-     * before the uiContext (/onos/ui/) and uses that as an app
+     * before the UICONTEXT (/onos/ui/) and uses that as an app
      * prefix by inserting it back into the WS URL.
      * If no prefix, then no insert.
      */
-    urlBase(protocol: string, port: string, host: string): string {
-        const match = window.location.href.match('.*//[^/]+/(.+)' + uiContext);
+    urlBase(protocol: string, port: string = '', host: string = ''): string {
+        const match = this.w.location.href.match('.*//[^/]+/(.+)' + UICONTEXT);
         const appPrefix = match ? '/' + match[1] : '';
 
-        return this.matchSecure(protocol) + '://' +
-            (host || window.location.hostname) + ':' +
-            (port || window.location.port) + appPrefix;
+        return this.matchSecure(protocol) +
+            '://' +
+            (host === '' ? this.w.location.hostname : host) +
+            ':' +
+            (port === '' ? this.w.location.port : port) +
+            appPrefix;
     }
 
     httpPrefix(suffix: string): string {
-        return this.urlBase('http', '', '') + suffix;
+        return this.urlBase('http') + suffix;
     }
 
-    wsPrefix(suffix: string, wsport: any, host: string): string {
+    wsPrefix(suffix: string, wsport: string, host: string): string {
         return this.urlBase('ws', wsport, host) + suffix;
     }
 
     rsUrl(path: string): string {
-        return this.httpPrefix(rsSuffix) + path;
+        return this.httpPrefix(RSSUFFIX) + path;
     }
 
-    wsUrl(path: string, wsport: any, host: string): string {
-        return this.wsPrefix(wsSuffix, wsport, host) + path;
+    wsUrl(path: string, wsport?: string, host?: string): string {
+        return this.wsPrefix(WSSUFFIX, wsport, host) + path;
     }
 }
diff --git a/web/gui2/src/main/webapp/app/fw/remote/websocket.service.ts b/web/gui2/src/main/webapp/app/fw/remote/websocket.service.ts
index 0691c72..e470ec1 100644
--- a/web/gui2/src/main/webapp/app/fw/remote/websocket.service.ts
+++ b/web/gui2/src/main/webapp/app/fw/remote/websocket.service.ts
@@ -18,37 +18,111 @@
 import { GlyphService } from '../svg/glyph.service';
 import { LogService } from '../../log.service';
 import { UrlFnService } from './urlfn.service';
+import { VeilComponent } from '../layer/veil/veil.component';
 import { WSock } from './wsock.service';
 
 /**
  * Event Type structure for the WebSocketService
  */
-interface EventType {
+export interface EventType {
     event: string;
-    payload: any;
+    payload: Object;
+}
+
+export interface Callback {
+    id: number;
+    error: string;
+    cb(host: string, url: string): void;
+}
+
+interface ClusterNode {
+    id: string;
+    ip: string;
+    m_uiAttached: boolean;
+}
+
+interface Bootstrap {
+    user: string;
+    clusterNodes: ClusterNode[];
+    glyphs: any[]; // TODO: narrow this down to a known type
+}
+
+interface ErrorData {
+    message: string;
+}
+
+export interface WsOptions {
+    wsport: number;
 }
 
 /**
  * ONOS GUI -- Remote -- Web Socket Service
+ *
+ * To see debug messages add ?debug=txrx to the URL
  */
-@Injectable()
+@Injectable({
+  providedIn: 'root',
+})
 export class WebSocketService {
     // internal state
-    private webSockOpts; // web socket options
-    private ws = null; // web socket reference
-    private wsUp = false; // web socket is good to go
-    private handlers = {}; // event handler bindings
+    private webSockOpts: WsOptions; // web socket options
+    private ws: WebSocket = null; // web socket reference
+    private wsUp: boolean = false; // web socket is good to go
+
+    // A map of event handler bindings - names and functions (that accept data and return void)
+    private handlers = new Map<string, (data: any) => void>([]);
     private pendingEvents: EventType[] = []; // events TX'd while socket not up
     private host: string; // web socket host
     private url; // web socket URL
-    private clusterNodes = []; // ONOS instances data for failover
+    private clusterNodes: ClusterNode[] = []; // ONOS instances data for failover
     private clusterIndex = -1; // the instance to which we are connected
     private glyphs = [];
-    private connectRetries = 0; // limit our attempts at reconnecting
-    private openListeners = {}; // registered listeners for websocket open()
-    private nextListenerId = 1; // internal ID for open listeners
-    private loggedInUser = null; // name of logged-in user
+    private connectRetries: number = 0; // limit our attempts at reconnecting
 
+    // A map of registered Callbacks for websocket open()
+    private openListeners = new Map<number, Callback>([]);
+    private nextListenerId: number = 1; // internal ID for open listeners
+    private loggedInUser = null; // name of logged-in user
+    private lcd: any; // The loading component delegate
+    private vcd: any; // The veil component delegate
+
+    /**
+     * built-in handler for the 'boostrap' event
+     */
+    private bootstrap(data: Bootstrap) {
+        this.log.debug('bootstrap data', data);
+        this.loggedInUser = data.user;
+
+        this.clusterNodes = data.clusterNodes;
+        this.clusterNodes.forEach((d, i) => {
+            if (d.m_uiAttached) {
+                this.clusterIndex = i;
+                this.log.info('Connected to cluster node ' + d.ip);
+                // TODO: add connect info to masthead somewhere
+            }
+        });
+        this.glyphs = data.glyphs;
+        const glyphsMap = new Map<string, string>([]);
+        this.glyphs.forEach((d, i) => {
+            glyphsMap.set('_' + d.id, d.viewbox);
+            glyphsMap.set(d.id, d.path);
+            this.gs.registerGlyphs(glyphsMap);
+        });
+    }
+
+    private error(data: ErrorData) {
+        const m: string = data.message || 'error from server';
+        this.log.error(m, data);
+
+        // Unrecoverable error - throw up the veil...
+        if (this.vcd) {
+            this.vcd.show([
+                'Oops!',
+                'Server reports error...',
+                m,
+            ]);
+        }
+    }
 
     constructor(
         private fs: FnService,
@@ -59,90 +133,319 @@
         private window: Window
     ) {
         this.log.debug(window.location.hostname);
+
+        // Bind the boot strap event by default
+        this.bindHandlers(new Map<string, (data) => void>([
+            ['bootstrap', (data) => this.bootstrap(data)],
+            ['error', (data) => this.error(data)]
+        ]));
+
         this.log.debug('WebSocketService constructed');
     }
 
+
+    // ==========================
+    // === Web socket callbacks
+
+    /**
+     * Called when WebSocket has just opened
+     *
+     * Lift the Veil if it is displayed
+     * If there are any events pending, send them
+     * Mark the WSS as up and inform any listeners for this open event
+     */
+    handleOpen(): void {
+        this.log.info('Web socket open - ', this.url);
+        // Hide the veil
+        if (this.vcd) {
+            this.vcd.hide();
+        }
+
+        if (this.fs.debugOn('txrx')) {
+            this.log.debug('Sending ' + this.pendingEvents.length + ' pending event(s)...');
+        }
+        this.pendingEvents.forEach((ev) => {
+            this.send(ev);
+        });
+        this.pendingEvents = [];
+
+        this.connectRetries = 0;
+        this.wsUp = true;
+        this.informListeners(this.host, this.url);
+    }
+
+    /**
+     * Function called when WebSocket send a message
+     */
+    handleMessage(msgEvent: MessageEvent): void {
+        let ev: EventType;
+        let h;
+        try {
+            ev = JSON.parse(msgEvent.data);
+        } catch (e) {
+            this.log.error('Message.data is not valid JSON', msgEvent.data, e);
+            return null;
+        }
+        if (this.fs.debugOn('txrx')) {
+            this.log.debug(' << *Rx* ', ev.event, ev.payload);
+        }
+        h = this.handlers.get(ev.event);
+        if (h) {
+            try {
+                h(ev.payload);
+            } catch (e) {
+                this.log.error('Problem handling event:', ev, e);
+                return null;
+            }
+        } else {
+            this.log.warn('Unhandled event:', ev);
+        }
+    }
+
+    /**
+     * Called by the WebSocket if it is closed from the server end
+     *
+     * If the loading component is shown, call stop() on it
+     * Try to find another node in the cluster to connect to
+     * If this is not possible then show the Veil Component
+     */
+    handleClose(): void {
+        this.log.warn('Web socket closed');
+        if (this.lcd) {
+            this.lcd.stop();
+        }
+        this.wsUp = false;
+        let gsucc;
+
+        if (gsucc = this.findGuiSuccessor()) {
+            this.url = this.createWebSocket(this.webSockOpts, gsucc);
+        } else {
+            // If no controllers left to contact, show the Veil...
+            if (this.vcd) {
+                this.vcd.show([
+                    'Oops!',  // TODO: Localize this
+                    'Web-socket connection to server closed...',
+                    'Try refreshing the page.',
+                ]);
+            }
+        }
+    }
+
+    // ==============================
+    // === Private Helper Functions
+
+    /**
+     * Find the next node in the ONOS cluster.
+     *
+     * This is used if the WebSocket connection closes because a
+     * node in the cluster ges down - fail over should be automatic
+     */
+    findGuiSuccessor(): string {
+        const ncn = this.clusterNodes.length;
+        let ip: string;
+        let node;
+
+        while (this.connectRetries < ncn && !ip) {
+            this.connectRetries++;
+            this.clusterIndex = (this.clusterIndex + 1) % ncn;
+            node = this.clusterNodes[this.clusterIndex];
+            ip = node && node.ip;
+        }
+
+        return ip;
+    }
+
+    /**
+     * When the WebSocket is opened, inform any listeners that registered
+     * for that event
+     */
+    informListeners(host: string, url: string): void {
+        for (const [key, cb] of this.openListeners.entries()) {
+            cb.cb(host, url);
+        }
+    }
+
+    send(ev: EventType): void {
+        if (this.fs.debugOn('txrx')) {
+            this.log.debug(' *Tx* >> ', ev.event, ev.payload);
+        }
+        this.ws.send(JSON.stringify(ev));
+    }
+
+    /**
+     * Check if there are no WSS event handlers left
+     */
+    noHandlersWarn(handlers: Map<string, Object>, caller: string): boolean {
+        if (!handlers || handlers.size === 0) {
+            this.log.warn('WSS.' + caller + '(): no event handlers');
+            return true;
+        }
+        return false;
+    }
+
     /* ===================
      * === API Functions
-     *
+     */
+
+    /**
      * Required for unit tests to set to known state
      */
-    resetState() {
+    resetState(): void {
         this.webSockOpts = undefined;
         this.ws = null;
         this.wsUp = false;
         this.host = undefined;
         this.url = undefined;
         this.pendingEvents = [];
-        this.handlers = {};
+        this.handlers.clear();
         this.clusterNodes = [];
         this.clusterIndex = -1;
         this.glyphs = [];
         this.connectRetries = 0;
-        this.openListeners = {};
+        this.openListeners.clear();
         this.nextListenerId = 1;
 
     }
 
-    /* Currently supported opts:
+    /*
+     * Currently supported opts:
      *  wsport: web socket port (other than default 8181)
      *  host:   if defined, is the host address to use
      */
-    createWebSocket(opts, _host_: string = '') {
-        const wsport = (opts && opts.wsport) || null;
-
+    createWebSocket(opts?: WsOptions, host?: string) {
         this.webSockOpts = opts; // preserved for future calls
+        this.host = host === undefined ? this.window.location.host : host;
+        this.url = this.ufs.wsUrl('core', opts === undefined ? '' : opts['wsport'].toString(), host);
 
-//        this.host = _host_ || this.host();
-        const url = this.ufs.wsUrl('core', wsport, _host_);
-
-        this.log.debug('Attempting to open websocket to: ' + url);
-        this.ws = this.wsock.newWebSocket(url);
+        this.log.debug('Attempting to open websocket to: ' + this.url);
+        this.ws = this.wsock.newWebSocket(this.url);
         if (this.ws) {
-            this.ws.onopen = this.handleOpen();
-            this.ws.onmessage = this.handleMessage('???');
-            this.ws.onclose = this.handleClose();
+            // fat arrow => syntax means that the 'this' context passed will
+            // be of WebSocketService, not the WebSocket
+            this.ws.onopen = (() => this.handleOpen());
+            this.ws.onmessage = ((msgEvent) => this.handleMessage(msgEvent));
+            this.ws.onclose = (() => this.handleClose());
 
-//            sendEvent('authentication', { token: onosAuth });
-            this.sendEvent('authentication token', '');
+            this.sendEvent('authentication token', { token: 'testAuth' });
         }
         // Note: Wsock logs an error if the new WebSocket call fails
-        return url;
+        return this.url;
     }
 
-    handleOpen() {
-        this.log.debug('WebSocketService: handleOpen() not yet implemented');
+
+    /**
+     * Binds the message handlers to their message type (event type) as
+     *  specified in the given map. Note that keys are the event IDs; values
+     *  are either:
+     *     * the event handler function, or
+     *     * an API object which has an event handler for the key
+     */
+    bindHandlers(handlerMap: Map<string, (data) => void>): void {
+        const dups: string[] = [];
+
+        if (this.noHandlersWarn(handlerMap, 'bindHandlers')) {
+            return null;
+        }
+        for (const [eventId, api] of handlerMap) {
+            this.log.debug('Adding handler for ', eventId);
+            const fn = this.fs.isF(api) || this.fs.isF(api[eventId]);
+            if (!fn) {
+                this.log.warn(eventId + ' handler not a function');
+                return;
+            }
+
+            if (this.handlers.get(eventId)) {
+                dups.push(eventId);
+            } else {
+                this.handlers.set(eventId, fn);
+            }
+        }
+        if (dups.length) {
+            this.log.warn('duplicate bindings ignored:', dups);
+        }
     }
 
-    handleMessage(msgEvent: any) {
-        this.log.debug('WebSocketService: handleMessage() not yet implemented');
+    /**
+     * Unbinds the specified message handlers.
+     *   Expected that the same map will be used, but we only care about keys
+     */
+    unbindHandlers(handlerMap: Map<string, (data) => void>): void {
+        if (this.noHandlersWarn(handlerMap, 'unbindHandlers')) {
+            return null;
+        }
+
+        for (const [eventId, api] of handlerMap) {
+            this.handlers.delete(eventId);
+        }
     }
 
-    handleClose() {
-        this.log.debug('WebSocketService: handleClose() not yet implemented');
+    /**
+     * Add a listener function for listening for WebSocket opening.
+     * The function must give a host and url and return void
+     */
+    addOpenListener(callback: (host: string, url: string) => void ): Callback {
+        const id: number = this.nextListenerId++;
+        const cb = this.fs.isF(callback);
+        const o: Callback = <Callback>{ id: id, cb: cb };
+
+        if (cb) {
+            this.openListeners.set(id, o);
+        } else {
+            this.log.error('WSS.addOpenListener(): callback not a function');
+            o.error = 'No callback defined';
+        }
+        return o;
     }
 
-    /* Formulates an event message and sends it via the web-socket.
+    /**
+     * Remove a listener of WebSocket opening
+     */
+    removeOpenListener(lsnr: Callback): void {
+        const id = this.fs.isO(lsnr) && lsnr.id;
+        let o;
+
+        if (!id) {
+            this.log.warn('WSS.removeOpenListener(): invalid listener', lsnr);
+            return null;
+        }
+        o = this.openListeners[id];
+
+        if (o) {
+            this.openListeners.delete(id);
+        }
+    }
+
+    /**
+     * Formulates an event message and sends it via the web-socket.
      * If the websocket is not up yet, we store it in a pending list.
      */
-    sendEvent(evType, payload) {
+    sendEvent(evType: string, payload: Object ): void {
         const ev = <EventType> {
             event: evType,
             payload: payload
         };
 
         if (this.wsUp) {
-            this._send(ev);
+            this.send(ev);
         } else {
             this.pendingEvents.push(ev);
         }
     }
 
-    _send(ev: EventType) {
-        if (this.fs.debugOn('txrx')) {
-            this.log.debug(' *Tx* >> ', ev.event, ev.payload);
-        }
-        this.ws.send(JSON.stringify(ev));
+    /**
+     * Binds the veil service as a delegate.
+     */
+    setVeilDelegate(vd: VeilComponent): void {
+        this.vcd = vd;
+    }
+
+    /**
+     * Binds the loading service as a delegate
+     */
+    setLoadingDelegate(ld: any): void {
+        // TODO - Investigate changing Loading Service to LoadingComponent
+        this.log.debug('Loading delegate set', ld);
+        this.lcd = ld;
     }
 
 }
diff --git a/web/gui2/src/main/webapp/app/fw/remote/wsock.service.ts b/web/gui2/src/main/webapp/app/fw/remote/wsock.service.ts
index 176b5d3..86e0625 100644
--- a/web/gui2/src/main/webapp/app/fw/remote/wsock.service.ts
+++ b/web/gui2/src/main/webapp/app/fw/remote/wsock.service.ts
@@ -21,7 +21,9 @@
  *
  * This service provided specifically so that it can be mocked in unit tests.
  */
-@Injectable()
+@Injectable({
+  providedIn: 'root',
+})
 export class WSock {
 
   constructor(
diff --git a/web/gui2/src/main/webapp/app/fw/svg/icon.service.ts b/web/gui2/src/main/webapp/app/fw/svg/icon.service.ts
index 674cbbf..c6a81e9 100644
--- a/web/gui2/src/main/webapp/app/fw/svg/icon.service.ts
+++ b/web/gui2/src/main/webapp/app/fw/svg/icon.service.ts
@@ -222,7 +222,7 @@
             'xlink:href': '#' + glyphId,
             width: dim,
             height: dim,
-            transform: this.sus.translate(xlate, xlate),
+            transform: this.sus.translate([xlate], xlate),
         });
         return g;
     }
diff --git a/web/gui2/src/main/webapp/app/fw/svg/svgutil.service.ts b/web/gui2/src/main/webapp/app/fw/svg/svgutil.service.ts
index 51d5d2a..9cba079 100644
--- a/web/gui2/src/main/webapp/app/fw/svg/svgutil.service.ts
+++ b/web/gui2/src/main/webapp/app/fw/svg/svgutil.service.ts
@@ -22,7 +22,9 @@
  *
  * The SVG Util Service provides a miscellany of utility functions.
  */
-@Injectable()
+@Injectable({
+  providedIn: 'root',
+})
 export class SvgUtilService {
 
     constructor(
@@ -32,7 +34,7 @@
         this.log.debug('SvgUtilService constructed');
     }
 
-    translate(x, y) {
+    translate(x: number[], y?: any): string {
         if (this.fs.isA(x) && x.length === 2 && !y) {
             return 'translate(' + x[0] + ',' + x[1] + ')';
         }
diff --git a/web/gui2/src/main/webapp/app/fw/util/fn.service.ts b/web/gui2/src/main/webapp/app/fw/util/fn.service.ts
index 0726afc..d355ce9 100644
--- a/web/gui2/src/main/webapp/app/fw/util/fn.service.ts
+++ b/web/gui2/src/main/webapp/app/fw/util/fn.service.ts
@@ -13,13 +13,32 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import { Injectable } from '@angular/core';
+import { Injectable, Inject } from '@angular/core';
 import { ActivatedRoute, Router} from '@angular/router';
 import { LogService } from '../../log.service';
 
 // Angular>=2 workaround for missing definition
 declare const InstallTrigger: any;
 
+const matcher = /<\/?([a-zA-Z0-9]+)*(.*?)\/?>/igm;
+const whitelist: string[] = ['b', 'i', 'p', 'em', 'strong', 'br'];
+const evillist: string[] = ['script', 'style', 'iframe'];
+
+/**
+ * Used with the Window size function;
+ **/
+export interface WindowSize {
+    width: number;
+    height: number;
+}
+
+/**
+ * For the sanitize() and analyze() functions
+ */
+export interface Match {
+    full: string;
+    name: string;
+}
 
 // TODO Move all this trie stuff to its own class
 // Angular>=2 Tightened up on types to avoid compiler errors
@@ -133,13 +152,14 @@
 @Injectable()
 export class FnService {
     // internal state
-    debugFlags = new Map<string, boolean>([
+    private debugFlags = new Map<string, boolean>([
 //        [ "LoadingService", true ]
     ]);
 
     constructor(
         private route: ActivatedRoute,
-        private log: LogService
+        private log: LogService,
+        @Inject(Window) private w: Window
     ) {
         this.route.queryParams.subscribe(params => {
             const debugparam: string = params['debug'];
@@ -149,33 +169,155 @@
         log.debug('FnService constructed');
     }
 
-    isF(f) {
+    /**
+     * Test if an argument is a function
+     *
+     * Note: the need for this would go away if all functions
+     * were strongly typed
+     */
+    isF(f: any): any {
         return typeof f === 'function' ? f : null;
     }
 
-    isA(a) {
+    /**
+     * Test if an argument is an array
+     *
+     * Note: the need for this would go away if all arrays
+     * were strongly typed
+     */
+    isA(a: any): any {
     // NOTE: Array.isArray() is part of EMCAScript 5.1
         return Array.isArray(a) ? a : null;
     }
 
-    isS(s) {
+    /**
+     * Test if an argument is a string
+     *
+     * Note: the need for this would go away if all strings
+     * were strongly typed
+     */
+    isS(s: any): string {
         return typeof s === 'string' ? s : null;
     }
 
-    isO(o) {
+    /**
+     * Test if an argument is an object
+     *
+     * Note: the need for this would go away if all objects
+     * were strongly typed
+     */
+    isO(o: any): Object {
         return (o && typeof o === 'object' && o.constructor === Object) ? o : null;
     }
 
-//    contains: contains,
-//    areFunctions: areFunctions,
-//    areFunctionsNonStrict: areFunctionsNonStrict,
-//    windowSize: windowSize,
+    /**
+     * Test that an array contains an object
+     */
+    contains(a: any[], x: any): boolean {
+        return this.isA(a) && a.indexOf(x) > -1;
+    }
+
+    /**
+     * Returns width and height of window inner dimensions.
+     * offH, offW : offset width/height are subtracted, if present
+     */
+    windowSize(offH: number = 0, offW: number = 0): WindowSize {
+        return {
+            height: this.w.innerHeight - offH,
+            width: this.w.innerWidth - offW
+        };
+    }
+
+    /**
+     * Returns true if all names in the array are defined as functions
+     * on the given api object; false otherwise.
+     * Also returns false if there are properties on the api that are NOT
+     * listed in the array of names.
+     *
+     * This gets extra complicated when the api Object is an
+     * Angular service - while the functions can be retrieved
+     * by an indexed get, the ownProperties does not show the
+     * functions of the class. We have to dive in to the prototypes
+     * properties to get these - and even then we have to filter
+     * out the constructor and any member variables
+     */
+    areFunctions(api: Object, fnNames: string[]): boolean {
+        const fnLookup: Map<string, boolean> = new Map();
+        let extraFound: boolean = false;
+
+        if (!this.isA(fnNames)) {
+            return false;
+        }
+
+        const n: number = fnNames.length;
+        let i: number;
+        let name: string;
+
+        for (i = 0; i < n; i++) {
+            name = fnNames[i];
+            if (!this.isF(api[name])) {
+                return false;
+            }
+            fnLookup.set(name, true);
+        }
+
+        // check for properties on the API that are not listed in the array,
+        const keys = Object.getOwnPropertyNames(api);
+        if (keys.length === 0) {
+            return true;
+        }
+        // If the api is a class it will have a name,
+        //  else it will just be called 'Object'
+        const apiObjectName: string = api.constructor.name;
+        if (apiObjectName === 'Object') {
+            Object.keys(api).forEach((key) => {
+                if (!fnLookup.get(key)) {
+                    extraFound = true;
+                }
+            });
+        } else { // It is a class, so its functions will be in the child (prototype)
+            const pObj: Object = Object.getPrototypeOf(api);
+            for ( const key in Object.getOwnPropertyDescriptors(pObj) ) {
+                if (key === 'constructor') { // Filter out constructor
+                    continue;
+                }
+                const value = Object.getOwnPropertyDescriptor(pObj, key);
+                // Only compare functions. Look for any not given in the map
+                if (this.isF(value.value) && !fnLookup.get(key)) {
+                    extraFound = true;
+                }
+            }
+        }
+        return !extraFound;
+    }
+
+    /**
+     * Returns true if all names in the array are defined as functions
+     * on the given api object; false otherwise. This is a non-strict version
+     * that does not care about other properties on the api.
+     */
+    areFunctionsNonStrict(api, fnNames): boolean {
+        if (!this.isA(fnNames)) {
+            return false;
+        }
+        const n = fnNames.length;
+        let i;
+        let name;
+
+        for (i = 0; i < n; i++) {
+            name = fnNames[i];
+            if (!this.isF(api[name])) {
+                return false;
+            }
+        }
+        return true;
+    }
 
     /**
      * Returns true if current browser determined to be a mobile device
      */
     isMobile() {
-        const ua = window.navigator.userAgent;
+        const ua = this.w.navigator.userAgent;
         const patt = /iPhone|iPod|iPad|Silk|Android|BlackBerry|Opera Mini|IEMobile/;
         return patt.test(ua);
     }
@@ -184,10 +326,10 @@
      * Returns true if the current browser determined to be Chrome
      */
     isChrome() {
-        const isChromium = (window as any).chrome;
-        const vendorName = window.navigator.vendor;
+        const isChromium = (this.w as any).chrome;
+        const vendorName = this.w.navigator.vendor;
 
-        const isOpera = window.navigator.userAgent.indexOf('OPR') > -1;
+        const isOpera = this.w.navigator.userAgent.indexOf('OPR') > -1;
         return (isChromium !== null &&
         isChromium !== undefined &&
         vendorName === 'Google Inc.' &&
@@ -195,8 +337,8 @@
     }
 
     isChromeHeadless() {
-        const vendorName = window.navigator.vendor;
-        const headlessChrome = window.navigator.userAgent.indexOf('HeadlessChrome') > -1;
+        const vendorName = this.w.navigator.vendor;
+        const headlessChrome = this.w.navigator.userAgent.indexOf('HeadlessChrome') > -1;
 
         return (vendorName === 'Google Inc.' && headlessChrome === true);
     }
@@ -205,8 +347,8 @@
      * Returns true if the current browser determined to be Safari
      */
     isSafari() {
-        return (window.navigator.userAgent.indexOf('Safari') !== -1 &&
-        window.navigator.userAgent.indexOf('Chrome') === -1);
+        return (this.w.navigator.userAgent.indexOf('Safari') !== -1 &&
+        this.w.navigator.userAgent.indexOf('Chrome') === -1);
     }
 
     /**
@@ -217,13 +359,90 @@
     }
 
     /**
+     * search through an array of objects, looking for the one with the
+     * tagged property matching the given key. tag defaults to 'id'.
+     * returns the index of the matching object, or -1 for no match.
+     */
+    find(key: string, array: Object[], tag: string = 'id'): number {
+        let idx: number;
+        const n: number = array.length;
+
+        for (idx = 0 ; idx < n; idx++) {
+            const d: Object = array[idx];
+            if (d[tag] === key) {
+                return idx;
+            }
+        }
+        return -1;
+    }
+
+    /**
+     * search through array to find (the first occurrence of) item,
+     * returning its index if found; otherwise returning -1.
+     */
+    inArray(item: any, array: any[]): number {
+        if (this.isA(array)) {
+            for (let i = 0; i < array.length; i++) {
+                if (array[i] === item) {
+                    return i;
+                }
+            }
+        }
+        return -1;
+    }
+
+    /**
+     * remove (the first occurrence of) the specified item from the given
+     * array, if any. Return true if the removal was made; false otherwise.
+     */
+    removeFromArray(item: any, array: any[]): boolean {
+        const i: number = this.inArray(item, array);
+        if (i >= 0) {
+            array.splice(i, 1);
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * return true if the object is empty, return false otherwise
+     */
+    isEmptyObject(obj: Object): boolean {
+        for (const key in obj) {
+            if (true) { return false; }
+        }
+        return true;
+    }
+
+    /**
      * Return the given string with the first character capitalized.
      */
-    cap(s) {
+    cap(s: string): string {
         return s ? s[0].toUpperCase() + s.slice(1).toLowerCase() : s;
     }
 
     /**
+     * return the parameter without a px suffix
+     */
+    noPx(num: string): number {
+        return Number(num.replace(/px$/, ''));
+    }
+
+    /**
+     * return an element's given style property without px suffix
+     */
+    noPxStyle(elem: any, prop: string): number {
+        return Number(elem.style(prop).replace(/px$/, ''));
+    }
+
+    /**
+     * Return true if a str ends with suffix
+     */
+    endsWith(str: string, suffix: string) {
+        return str.indexOf(suffix, str.length - suffix.length) !== -1;
+    }
+
+    /**
      * output debug message to console, if debug tag set...
      * e.g. fs.debug('mytag', arg1, arg2, ...)
      */
@@ -233,7 +452,7 @@
         }
     }
 
-    parseDebugFlags(dbgstr: string): void {
+    private parseDebugFlags(dbgstr: string): void {
         const bits = dbgstr ? dbgstr.split(',') : [];
         bits.forEach((key) => {
             this.debugFlags.set(key, true);
@@ -248,4 +467,66 @@
         return this.debugFlags.get(tag);
     }
 
+
+
+    // -----------------------------------------------------------------
+    // The next section deals with sanitizing external strings destined
+    // to be loaded via a .html() function call.
+    //
+    // See definition of matcher, evillist and whitelist at the top of this file
+
+    /*
+     * Returns true if the tag is in the evil list, (and is not an end-tag)
+     */
+    inEvilList(tag: any): boolean {
+        return (evillist.indexOf(tag.name) !== -1 && tag.full.indexOf('/') === -1);
+    }
+
+    /*
+     * Returns an array of Matches of matcher in html
+     */
+    analyze(html: string): Match[] {
+        const matches: Match[] = [];
+        let match;
+
+        // extract all tags
+        while ((match = matcher.exec(html)) !== null) {
+            matches.push({
+                full: match[0],
+                name: match[1],
+                // NOTE: ignoring attributes {match[2].split(' ')} for now
+            });
+        }
+
+        return matches;
+    }
+
+    /*
+     * Returns a cleaned version of html
+     */
+    sanitize(html: string): string {
+        const matches: Match[] = this.analyze(html);
+
+        // completely obliterate evil tags and their contents...
+        evillist.forEach((tag) => {
+            const re = new RegExp('<' + tag + '(.*?)>(.*?[\r\n])*?(.*?)(.*?[\r\n])*?<\/' + tag + '>', 'gim');
+            html = html.replace(re, '');
+        });
+
+        // filter out all but white-listed tags and end-tags
+        matches.forEach((tag) => {
+            if (whitelist.indexOf(tag.name) === -1) {
+                html = html.replace(tag.full, '');
+                if (this.inEvilList(tag)) {
+                    this.log.warn('Unsanitary HTML input -- ' +
+                        tag.full + ' detected!');
+                }
+            }
+        });
+
+        // TODO: consider encoding HTML entities, e.g. '&' -> '&amp;'
+
+        return html;
+    }
+
 }
diff --git a/web/gui2/src/main/webapp/app/fw/util/lion.service.ts b/web/gui2/src/main/webapp/app/fw/util/lion.service.ts
index a33ef6b..5ab9f65 100644
--- a/web/gui2/src/main/webapp/app/fw/util/lion.service.ts
+++ b/web/gui2/src/main/webapp/app/fw/util/lion.service.ts
@@ -29,19 +29,43 @@
 /**
  * ONOS GUI -- Lion -- Localization Utilities
  */
-@Injectable()
+@Injectable({
+  providedIn: 'root',
+})
 export class LionService {
 
     ubercache: any[];
 
+    /**
+     * Handler for uberlion event from WSS
+     */
+    uberlion(data: Lion) {
+        this.ubercache = data.lion;
+
+        this.log.info('LION service: Locale... [' + data.locale + ']');
+        this.log.info('LION service: Bundles installed...');
+
+        for (const p in this.ubercache) {
+            if (this.ubercache[p]) {
+                this.log.info('            :=> ', p);
+            }
+        }
+
+        this.log.debug('LION service: uber-lion bundle received:', data);
+    }
+
     constructor(
         private log: LogService,
         private wss: WebSocketService
     ) {
+        this.wss.bindHandlers(new Map<string, (data) => void>([
+            ['uberlion', (data) => this.uberlion(data) ]
+        ]));
         this.log.debug('LionService constructed');
     }
 
-    /* returns a lion bundle (function) for the given bundle ID
+    /**
+     * Returns a lion bundle (function) for the given bundle ID (string)
      * returns a function that takes a string and returns a string
      */
     bundle(bundleId: string): (string) => string {
@@ -58,21 +82,4 @@
     getKey(key: string): string {
         return this.bundle[key] || '%' + key + '%';
     }
-
-    /* handler for uberlion event..
-     */
-    uberlion(data: Lion) {
-        this.ubercache = data.lion;
-
-        this.log.info('LION service: Locale... [' + data.locale + ']');
-        this.log.info('LION service: Bundles installed...');
-
-        for (const p in this.ubercache) {
-            if (this.ubercache[p]) {
-                this.log.info('            :=> ', p);
-            }
-        }
-
-        this.log.debug('LION service: uber-lion bundle received:', data);
-    }
 }
diff --git a/web/gui2/src/main/webapp/app/onos.common.css b/web/gui2/src/main/webapp/app/onos.common.css
new file mode 100644
index 0000000..9a3f5a6
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/onos.common.css
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2015-present Open Networking Foundation
+ *
+ * Licensed 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.
+ */
+
+/*
+ ONOS GUI -- common -- CSS file
+ */
+
+.clickable {
+    cursor: pointer;
+}
+
+.light .editable {
+    border-bottom: 1px dashed #ca504b;
+}
+
+.dark .editable {
+    border-bottom: 1px dashed #df4f4a;
+}
+
+.light svg.embeddedIcon .icon .glyph {
+    fill: #0071bd;
+}
+
+.dark svg.embeddedIcon .icon .glyph {
+    fill: #375b7f;
+}
diff --git a/web/gui2/src/main/webapp/app/onos.component.css b/web/gui2/src/main/webapp/app/onos.component.css
index e69de29..60933d8 100644
--- a/web/gui2/src/main/webapp/app/onos.component.css
+++ b/web/gui2/src/main/webapp/app/onos.component.css
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2014-present Open Networking Foundation
+ *
+ * Licensed 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.
+ */
+
+/*
+ ONOS GUI -- core (layout) -- CSS file
+ */
+
+#view {
+    padding: 6px;
+    width: 100%;
+    height: 100%;
+}
+
+#view h2 {
+    -webkit-margin-before: 0;
+    -webkit-margin-after: 0;
+    margin: 32px 0 4px 16px;
+    padding: 0;
+    font-size: 18pt;
+    font-weight: lighter;
+}
diff --git a/web/gui2/src/main/webapp/app/onos.component.html b/web/gui2/src/main/webapp/app/onos.component.html
index 566c972..42d2bdd 100644
--- a/web/gui2/src/main/webapp/app/onos.component.html
+++ b/web/gui2/src/main/webapp/app/onos.component.html
@@ -1,6 +1,8 @@
 <div style="text-align:center" onosDetectBrowser>
     <onos-mast></onos-mast>
     <onos-nav></onos-nav>
+    <onos-veil #veil></onos-veil>
+    <div>{{ wss.setVeilDelegate(veil) }}</div>
     <router-outlet></router-outlet>
 </div>
 
diff --git a/web/gui2/src/main/webapp/app/onos.component.ts b/web/gui2/src/main/webapp/app/onos.component.ts
index 02b3a44..bbec955 100644
--- a/web/gui2/src/main/webapp/app/onos.component.ts
+++ b/web/gui2/src/main/webapp/app/onos.component.ts
@@ -19,11 +19,10 @@
 import { KeyService } from './fw/util/key.service';
 import { ThemeService } from './fw/util/theme.service';
 import { GlyphService } from './fw/svg/glyph.service';
-import { VeilService } from './fw/layer/veil.service';
 import { PanelService } from './fw/layer/panel.service';
 import { QuickHelpService } from './fw/layer/quickhelp.service';
 import { EeService } from './fw/util/ee.service';
-import { WebSocketService } from './fw/remote/websocket.service';
+import { WebSocketService, WsOptions } from './fw/remote/websocket.service';
 import { SpriteService } from './fw/svg/sprite.service';
 import { OnosService, View } from './onos.service';
 
@@ -61,7 +60,7 @@
 @Component({
   selector: 'onos-root',
   templateUrl: './onos.component.html',
-  styleUrls: ['./onos.component.css']
+  styleUrls: ['./onos.component.css', './onos.common.css']
 })
 export class OnosComponent implements OnInit {
     public title = 'onos';
@@ -81,7 +80,6 @@
         private ks: KeyService,
         private ts: ThemeService,
         private gs: GlyphService,
-        private vs: VeilService,
         private ps: PanelService,
         private qhs: QuickHelpService,
         private ee: EeService,
@@ -101,6 +99,8 @@
         log.warn('OnosComponent: testing logger.warn()');
         log.error('OnosComponent: testing logger.error()');
 
+        this.wss.createWebSocket(<WsOptions>{ wsport: 8181});
+
         log.debug('OnosComponent constructed');
     }
 
diff --git a/web/gui2/src/main/webapp/app/onos.css b/web/gui2/src/main/webapp/app/onos.css
deleted file mode 100644
index fa485ac..0000000
--- a/web/gui2/src/main/webapp/app/onos.css
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
- * Copyright 2014-present Open Networking Foundation
- *
- * Licensed 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.
- */
-
-/*
- ONOS GUI -- core (layout) -- CSS file
- */
-
-html {
-    font-family: 'Open Sans', sans-serif;
-    -webkit-text-size-adjust: 100%;
-    -ms-text-size-adjust: 100%;
-    height: 100%;
-}
-
-/*
-   overflow hidden is to ensure that the body does not expand to account
-   for any flyout panes, that are positioned "off screen".
- */
-body {
-    height: 100%;
-    margin: 0;
-    overflow: hidden;
-}
-
-#view {
-    padding: 6px;
-    width: 100%;
-    height: 100%;
-}
-
-#view h2 {
-    -webkit-margin-before: 0;
-    -webkit-margin-after: 0;
-    margin: 32px 0 4px 16px;
-    padding: 0;
-    font-size: 18pt;
-    font-weight: lighter;
-}
diff --git a/web/gui2/src/main/webapp/app/view/device/device.component.ts b/web/gui2/src/main/webapp/app/view/device/device.component.ts
index db42c4d..6929f9b 100644
--- a/web/gui2/src/main/webapp/app/view/device/device.component.ts
+++ b/web/gui2/src/main/webapp/app/view/device/device.component.ts
@@ -58,7 +58,7 @@
     ngOnInit() {
         this.log.debug('DeviceComponent initialized');
         // TODO: Remove this - it's only for demo purposes
-        this.ls.startAnim();
+//        this.ls.startAnim();
     }
 
 }
diff --git a/web/gui2/src/main/webapp/app/view/device/device.module.ts b/web/gui2/src/main/webapp/app/view/device/device.module.ts
index 9cd50cb..8f0f351 100644
--- a/web/gui2/src/main/webapp/app/view/device/device.module.ts
+++ b/web/gui2/src/main/webapp/app/view/device/device.module.ts
@@ -17,7 +17,7 @@
 import { CommonModule } from '@angular/common';
 import { DeviceComponent } from './device.component';
 import { DeviceDetailsPanelDirective } from './devicedetailspanel.directive';
-
+import { RemoteModule } from '../../fw/remote/remote.module';
 /**
  * ONOS GUI -- Device View Module
  */
@@ -26,7 +26,8 @@
     DeviceComponent
   ],
   imports: [
-    CommonModule
+    CommonModule,
+    RemoteModule
   ],
   declarations: [
     DeviceComponent,
diff --git a/web/gui2/src/main/webapp/index.html b/web/gui2/src/main/webapp/index.html
index 83d2b6c..9ca6a02 100644
--- a/web/gui2/src/main/webapp/index.html
+++ b/web/gui2/src/main/webapp/index.html
@@ -26,6 +26,7 @@
 
     <link href='https://fonts.googleapis.com/css?family=Open+Sans:400,300,700'
           rel='stylesheet' type='text/css'>
+    <link href="onos.theme.css" type='text/css'>
     <link href="app/fw/layer/loading.service.css" rel='stylesheet' type='text/css'>
     <base href="/">
     <title>ONOS</title>
diff --git a/web/gui2/src/main/webapp/onos.theme.css b/web/gui2/src/main/webapp/onos.theme.css
new file mode 100644
index 0000000..dc14c80
--- /dev/null
+++ b/web/gui2/src/main/webapp/onos.theme.css
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2016-present Open Networking Foundation
+ *
+ * Licensed 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.
+ */
+
+/*
+ ONOS GUI -- core (theme) -- CSS file
+ */
+
+body {
+    background-color: white;
+}
+
+html {
+    font-family: 'Open Sans', sans-serif;
+    -webkit-text-size-adjust: 100%;
+    -ms-text-size-adjust: 100%;
+    height: 100%;
+}
+
+/*
+   overflow hidden is to ensure that the body does not expand to account
+   for any flyout panes, that are positioned "off screen".
+ */
+body {
+    height: 100%;
+    margin: 0;
+    overflow: hidden;
+}
+
+#view h2 {
+    color: #3c3a3a;
+}
+
+a {
+    color: #009fdb;
+    text-decoration: none;
+}
+a:hover {
+    text-decoration: underline;
+}
+a:visited {
+    color: #7fabdb;
+    text-decoration: none;
+}
+
+/* ========== DARK Theme ========== */
+
+body.dark {
+    background-color: #282528;
+}
+
+.dark #view h2 {
+    color: #6a6e6a;
+}
+
+.dark a {
+    color: #007ca6;
+}
+.dark a:visited {
+    color: #4f6e90;
+}
+
+.dark input {
+    color: #dddddd;
+    background-color: #222222;
+    border: 1px solid #666666;
+}
+
+.dark select {
+    color: #dddddd;
+    background-color: #222222;
+}
\ No newline at end of file
diff --git a/web/gui2/src/main/webapp/tests/app/detectbrowser.directive.spec.ts b/web/gui2/src/main/webapp/tests/app/detectbrowser.directive.spec.ts
index 06715cd..9183536 100644
--- a/web/gui2/src/main/webapp/tests/app/detectbrowser.directive.spec.ts
+++ b/web/gui2/src/main/webapp/tests/app/detectbrowser.directive.spec.ts
@@ -24,8 +24,8 @@
 import { of } from 'rxjs';
 
 class MockFnService extends FnService {
-    constructor(ar: ActivatedRoute, log: LogService) {
-        super(ar, log);
+    constructor(ar: ActivatedRoute, log: LogService, w: Window) {
+        super(ar, log, w);
     }
 }
 
@@ -44,17 +44,25 @@
 describe('DetectBrowserDirective', () => {
     let log: LogService;
     let ar: ActivatedRoute;
+    let mockWindow: Window;
 
     beforeEach(() => {
         log = new ConsoleLoggerService();
         ar = new MockActivatedRoute(['debug', 'DetectBrowserDirective']);
+        mockWindow = <any>{
+            navigator: {
+                userAgent: 'HeadlessChrome',
+                vendor: 'Google Inc.'
+            }
+        };
 
         TestBed.configureTestingModule({
             providers: [ DetectBrowserDirective,
-                { provide: FnService, useValue: new MockFnService(ar, log) },
+                { provide: FnService, useValue: new MockFnService(ar, log, mockWindow) },
                 { provide: LogService, useValue: log },
                 { provide: OnosService, useClass: MockOnosService },
                 { provide: Document, useValue: document },
+                { provide: Window, useFactory: (() => mockWindow ) }
             ]
         });
     });
diff --git a/web/gui2/src/main/webapp/tests/app/fw/layer/veil.service.spec.ts b/web/gui2/src/main/webapp/tests/app/fw/layer/veil.service.spec.ts
deleted file mode 100644
index 3d5509b..0000000
--- a/web/gui2/src/main/webapp/tests/app/fw/layer/veil.service.spec.ts
+++ /dev/null
@@ -1,57 +0,0 @@
-/*
- * Copyright 2015-present Open Networking Foundation
- *
- * Licensed 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.
- */
-import { TestBed, inject } from '@angular/core/testing';
-
-import { LogService } from '../../../../app/log.service';
-import { ConsoleLoggerService } from '../../../../app/consolelogger.service';
-import { FnService } from '../../../../app/fw/util/fn.service';
-import { GlyphService } from '../../../../app/fw/svg/glyph.service';
-import { KeyService } from '../../../../app/fw/util/key.service';
-import { VeilService } from '../../../../app/fw/layer/veil.service';
-import { WebSocketService } from '../../../../app/fw/remote/websocket.service';
-
-class MockFnService {}
-
-class MockGlyphService {}
-
-class MockKeyService {}
-
-class MockWebSocketService {}
-
-/**
- * ONOS GUI -- Layer -- Veil Service - Unit Tests
- */
-describe('VeilService', () => {
-    let log: LogService;
-
-    beforeEach(() => {
-        log = new ConsoleLoggerService();
-
-        TestBed.configureTestingModule({
-            providers: [VeilService,
-                { provide: FnService, useClass: MockFnService },
-                { provide: GlyphService, useClass: MockGlyphService },
-                { provide: KeyService, useClass: MockKeyService },
-                { provide: LogService, useValue: log },
-                { provide: WebSocketService, useClass: MockWebSocketService },
-            ]
-        });
-    });
-
-    it('should be created', inject([VeilService], (service: VeilService) => {
-        expect(service).toBeTruthy();
-    }));
-});
diff --git a/web/gui2/src/main/webapp/tests/app/fw/remote/urlfn.service.spec.ts b/web/gui2/src/main/webapp/tests/app/fw/remote/urlfn.service.spec.ts
index a54458b..6b229bd 100644
--- a/web/gui2/src/main/webapp/tests/app/fw/remote/urlfn.service.spec.ts
+++ b/web/gui2/src/main/webapp/tests/app/fw/remote/urlfn.service.spec.ts
@@ -18,26 +18,106 @@
 import { LogService } from '../../../../app/log.service';
 import { ConsoleLoggerService } from '../../../../app/consolelogger.service';
 import { UrlFnService } from '../../../../app/fw/remote/urlfn.service';
+import { FnService } from '../../../../app/fw/util/fn.service';
+import { ActivatedRoute, Params } from '@angular/router';
+import { of } from 'rxjs';
+
+class MockActivatedRoute extends ActivatedRoute {
+    constructor(params: Params) {
+        super();
+        this.queryParams = of(params);
+    }
+}
 
 /**
  * ONOS GUI -- Remote -- General Functions - Unit Tests
  */
 describe('UrlFnService', () => {
     let log: LogService;
-    const windowMock = <any>{ location: <any> { hostname: 'localhost' } };
+    let ufs: UrlFnService;
+    let fs: FnService;
+    let ar: MockActivatedRoute;
+    let windowMock: Window;
 
     beforeEach(() => {
         log = new ConsoleLoggerService();
+        ar = new MockActivatedRoute({'debug': 'TestService'});
+        windowMock = <any>{
+            location: <any> {
+                hostname: '',
+                host: '',
+                port: '',
+                protocol: '',
+                search: { debug: 'true'},
+                href: ''
+            }
+        };
+
+        fs = new FnService(ar, log, windowMock);
 
         TestBed.configureTestingModule({
             providers: [UrlFnService,
                 { provide: LogService, useValue: log },
-                { provide: Window, useValue: windowMock },
+                { provide: Window, useFactory: (() => windowMock ) },
             ]
         });
+
+        ufs = TestBed.get(UrlFnService);
     });
 
-    it('should be created', inject([UrlFnService], (service: UrlFnService) => {
-        expect(service).toBeTruthy();
-    }));
+    function setLoc(prot: string, h: string, p: string, ctx: string = '') {
+        windowMock.location.host = h;
+        windowMock.location.hostname = h;
+        windowMock.location.port = p;
+        windowMock.location.protocol = prot;
+        windowMock.location.href = prot + '://' + h + ':' + p +
+            ctx + '/onos/ui/';
+    }
+
+    it('should define UrlFnService', () => {
+        expect(ufs).toBeDefined();
+    });
+
+    it('should define api functions', () => {
+        expect(fs.areFunctions(ufs, [
+            'rsUrl', 'wsUrl', 'urlBase', 'httpPrefix',
+            'wsPrefix', 'matchSecure'
+        ])).toBeTruthy();
+    });
+
+    it('should return the correct (http) RS url', () => {
+        setLoc('http', 'foo', '123');
+        expect(ufs.rsUrl('path')).toEqual('http://foo:123/onos/ui/rs/path');
+    });
+
+    it('should return the correct (https) RS url', () => {
+        setLoc('https', 'foo', '123');
+        expect(ufs.rsUrl('path')).toEqual('https://foo:123/onos/ui/rs/path');
+    });
+
+    it('should return the correct (ws) WS url', () => {
+        setLoc('http', 'foo', '123');
+        expect(ufs.wsUrl('path')).toEqual('ws://foo:123/onos/ui/websock/path');
+    });
+
+    it('should return the correct (wss) WS url', () => {
+        setLoc('https', 'foo', '123');
+        expect(ufs.wsUrl('path')).toEqual('wss://foo:123/onos/ui/websock/path');
+    });
+
+    it('should allow us to define an alternate WS port', () => {
+        setLoc('http', 'foo', '123');
+        expect(ufs.wsUrl('xyyzy', '456')).toEqual('ws://foo:456/onos/ui/websock/xyyzy');
+    });
+
+    it('should allow us to define an alternate host', () => {
+        setLoc('http', 'foo', '123');
+        expect(ufs.wsUrl('core', '456', 'bar')).toEqual('ws://bar:456/onos/ui/websock/core');
+    });
+
+    it('should allow us to inject an app context', () => {
+        setLoc('http', 'foo', '123', '/my/app');
+        expect(ufs.wsUrl('path')).toEqual('ws://foo:123/my/app/onos/ui/websock/path');
+    });
+
 });
diff --git a/web/gui2/src/main/webapp/tests/app/fw/remote/websocket.service.spec.ts b/web/gui2/src/main/webapp/tests/app/fw/remote/websocket.service.spec.ts
index d18dda4..5c8d6b7 100644
--- a/web/gui2/src/main/webapp/tests/app/fw/remote/websocket.service.spec.ts
+++ b/web/gui2/src/main/webapp/tests/app/fw/remote/websocket.service.spec.ts
@@ -16,44 +16,266 @@
 import { TestBed, inject } from '@angular/core/testing';
 
 import { LogService } from '../../../../app/log.service';
-import { ConsoleLoggerService } from '../../../../app/consolelogger.service';
-import { WebSocketService } from '../../../../app/fw/remote/websocket.service';
+import { WebSocketService, WsOptions, Callback, EventType } from '../../../../app/fw/remote/websocket.service';
 import { FnService } from '../../../../app/fw/util/fn.service';
 import { GlyphService } from '../../../../app/fw/svg/glyph.service';
+import { ActivatedRoute, Params } from '@angular/router';
 import { UrlFnService } from '../../../../app/fw/remote/urlfn.service';
 import { WSock } from '../../../../app/fw/remote/wsock.service';
+import { of } from 'rxjs';
 
-class MockFnService {}
+class MockActivatedRoute extends ActivatedRoute {
+    constructor(params: Params) {
+        super();
+        this.queryParams = of(params);
+    }
+}
 
 class MockGlyphService {}
 
-class MockUrlFnService {}
-
 class MockWSock {}
 
 /**
  * ONOS GUI -- Remote -- Web Socket Service - Unit Tests
  */
 describe('WebSocketService', () => {
-    let log: LogService;
-    const windowMock = <any>{ location: <any> { hostname: 'localhost' } };
+    let wss: WebSocketService;
+    let fs: FnService;
+    let ar: MockActivatedRoute;
+    let windowMock: Window;
+    let logServiceSpy: jasmine.SpyObj<LogService>;
+
+    const noop = () => ({});
+    const send = jasmine.createSpy('send')
+        .and.callFake((ev) => ev);
+    const mockWebSocket = {
+        send: send,
+        onmessage: (msgEvent) => ({}),
+        onopen: () => ({}),
+        onclose: () => ({}),
+    };
 
     beforeEach(() => {
-        log = new ConsoleLoggerService();
+        const logSpy = jasmine.createSpyObj('LogService', ['info', 'debug', 'warn', 'error']);
+        ar = new MockActivatedRoute({'debug': 'txrx'});
+
+        windowMock = <any>{
+            location: <any> {
+                hostname: 'foo',
+                host: 'foo',
+                port: '80',
+                protocol: 'http',
+                search: { debug: 'true'},
+                href: 'ws://foo:123/onos/ui/websock/path',
+                absUrl: 'ws://foo:123/onos/ui/websock/path'
+            }
+        };
+        fs = new FnService(ar, logSpy, windowMock);
 
         TestBed.configureTestingModule({
             providers: [WebSocketService,
-                { provide: FnService, useClass: MockFnService },
-                { provide: LogService, useValue: log },
+                { provide: FnService, useValue: fs },
+                { provide: LogService, useValue: logSpy },
                 { provide: GlyphService, useClass: MockGlyphService },
-                { provide: UrlFnService, useClass: MockUrlFnService },
-                { provide: WSock, useClass: MockWSock },
-                { provide: Window, useValue: windowMock },
+                { provide: UrlFnService, useValue: new UrlFnService(logSpy, windowMock) },
+                { provide: Window, useFactory: (() => windowMock ) },
+                { provide: WSock, useFactory: (() => {
+                        return {
+                            newWebSocket: (() => mockWebSocket)
+                        };
+                    })
+                }
             ]
         });
+
+        wss = TestBed.get(WebSocketService);
+        logServiceSpy = TestBed.get(LogService);
     });
 
-    it('should be created', inject([WebSocketService], (service: WebSocketService) => {
-        expect(service).toBeTruthy();
-    }));
+    it('should define WebSocketService', () => {
+        expect(wss).toBeDefined();
+    });
+
+    it('should define api functions', () => {
+        expect(fs.areFunctions(wss, ['bootstrap', 'error',
+            'handleOpen', 'handleMessage', 'handleClose',
+            'findGuiSuccessor', 'informListeners', 'send',
+            'noHandlersWarn', 'resetState',
+            'createWebSocket', 'bindHandlers', 'unbindHandlers',
+            'addOpenListener', 'removeOpenListener', 'sendEvent',
+            'setVeilDelegate', 'setLoadingDelegate'
+        ])).toBeTruthy();
+    });
+
+    it('should use the appropriate URL, createWebsocket', () => {
+        const url = wss.createWebSocket();
+        expect(url).toEqual('ws://foo:80/onos/ui/websock/core');
+    });
+
+    it('should use the appropriate URL with modified port, createWebsocket',
+        () => {
+            const url = wss.createWebSocket(<WsOptions>{ wsport: 1243 });
+            expect(url).toEqual('ws://foo:1243/onos/ui/websock/core');
+    });
+
+    it('should verify websocket event handlers, createWebsocket', () => {
+        wss.createWebSocket({ wsport: 1234 });
+        expect(fs.isF(mockWebSocket.onopen)).toBeTruthy();
+        expect(fs.isF(mockWebSocket.onmessage)).toBeTruthy();
+        expect(fs.isF(mockWebSocket.onclose)).toBeTruthy();
+    });
+
+    it('should invoke listener callbacks when websocket is up, handleOpen',
+        () => {
+            let num = 0;
+            function incrementNum(host: string, url: string) {
+                expect(host).toEqual('foo');
+                num++;
+            }
+            wss.addOpenListener(incrementNum);
+            wss.createWebSocket({ wsport: 1234 });
+
+            mockWebSocket.onopen();
+            expect(num).toBe(1);
+    });
+
+    it('should send pending events, handleOpen', () => {
+        const fakeEvent = {
+            event: 'mockEv',
+            payload: { mock: 'thing' }
+        };
+        wss.sendEvent(fakeEvent.event, fakeEvent.payload);
+        // on opening the socket, a single authentication event should have
+        // been sent already...
+        expect(mockWebSocket.send.calls.count()).toEqual(1);
+
+        wss.createWebSocket({ wsport: 1234 });
+        mockWebSocket.onopen();
+        expect(mockWebSocket.send).toHaveBeenCalledWith(JSON.stringify(fakeEvent));
+    });
+
+    it('should handle an incoming bad JSON message, handleMessage', () => {
+        const badMsg = {
+            data: 'bad message'
+        };
+        wss.createWebSocket({ wsport: 1234 });
+        expect(mockWebSocket.onmessage(badMsg)).toBeNull();
+        expect(logServiceSpy.error).toHaveBeenCalled();
+    });
+
+    it('should verify message was handled, handleMessage', () => {
+        let num = 0;
+        function fakeHandler(data1: Object) { num++; }
+        const data = JSON.stringify(<EventType>{
+            event: 'mockEvResp',
+            payload: {}
+        });
+        const event = {
+            data: data
+        };
+
+        wss.createWebSocket({ wsport: 1234 });
+        wss.bindHandlers(new Map<string, (data) => void>([
+            ['mockEvResp', (data2) => fakeHandler(data2)]
+        ]));
+        expect(mockWebSocket.onmessage(event)).toBe(undefined);
+        expect(num).toBe(1);
+    });
+
+    it('should warn if there is an unhandled event, handleMessage', () => {
+        const data = { foo: 'bar', bar: 'baz'};
+        const dataString = JSON.stringify(data);
+        const badEv = {
+                data: dataString
+            };
+        wss.createWebSocket({ wsport: 1234 });
+        mockWebSocket.onmessage(badEv);
+        expect(logServiceSpy.warn).toHaveBeenCalledWith('Unhandled event:', data);
+    });
+
+    it('should not warn if valid input, bindHandlers', () => {
+        expect(wss.bindHandlers(new Map<string, (data) => void>([
+            ['test', noop ],
+            ['bar', noop ]
+        ]))).toBe(undefined);
+
+        expect(logServiceSpy.warn).not.toHaveBeenCalled();
+    });
+
+    it('should warn if no arguments, bindHandlers', () => {
+        expect(wss.bindHandlers(
+            new Map<string, (data) => void>([])
+        )).toBeNull();
+        expect(logServiceSpy.warn).toHaveBeenCalledWith(
+            'WSS.bindHandlers(): no event handlers'
+        );
+    });
+
+    it('should warn if duplicate handlers were given, bindHandlers',
+        () => {
+            wss.bindHandlers(
+                new Map<string, (data) => void>([
+                    ['noop', noop ]
+                ])
+            );
+            expect(wss.bindHandlers(
+                new Map<string, (data) => void>([
+                    ['noop', noop ]
+                ])
+            )).toBe(undefined);
+            expect(logServiceSpy.warn).toHaveBeenCalledWith('duplicate bindings ignored:',
+                                                    ['noop']);
+    });
+
+    it('should warn if no arguments, unbindHandlers', () => {
+        expect(wss.unbindHandlers(
+            new Map<string, (data) => void>([])
+        )).toBeNull();
+        expect(logServiceSpy.warn).toHaveBeenCalledWith(
+            'WSS.unbindHandlers(): no event handlers'
+        );
+    });
+    // Note: cannot test unbindHandlers' forEach due to it using closure variable
+
+    it('should not warn if valid argument, addOpenListener', () => {
+        let o = wss.addOpenListener(noop);
+        expect(o.id === 1);
+        expect(o.cb === noop);
+        expect(logServiceSpy.warn).not.toHaveBeenCalled();
+        o = wss.addOpenListener(noop);
+        expect(o.id === 2);
+        expect(o.cb === noop);
+        expect(logServiceSpy.warn).not.toHaveBeenCalled();
+    });
+
+    it('should log error if callback not a function, addOpenListener',
+        () => {
+            const o = wss.addOpenListener(null);
+            expect(o.id === 1);
+            expect(o.cb === null);
+            expect(o.error === 'No callback defined');
+            expect(logServiceSpy.error).toHaveBeenCalledWith(
+                'WSS.addOpenListener(): callback not a function'
+            );
+    });
+
+    it('should not warn if valid listener object, removeOpenListener', () => {
+        expect(wss.removeOpenListener(<Callback>{
+            id: 1,
+            error: 'error',
+            cb: noop
+        })).toBe(undefined);
+        expect(logServiceSpy.warn).not.toHaveBeenCalled();
+    });
+
+    it('should warn if listener is invalid, removeOpenListener', () => {
+        expect(wss.removeOpenListener(<Callback>{})).toBeNull();
+        expect(logServiceSpy.warn).toHaveBeenCalledWith(
+            'WSS.removeOpenListener(): invalid listener', {}
+        );
+    });
+
+    // Note: handleClose is not currently tested due to all work it does relies
+    //       on closure variables that cannot be mocked
+
 });
diff --git a/web/gui2/src/main/webapp/tests/app/fw/util/fn.service.spec.ts b/web/gui2/src/main/webapp/tests/app/fw/util/fn.service.spec.ts
index ff6ceaf..84b5f094 100644
--- a/web/gui2/src/main/webapp/tests/app/fw/util/fn.service.spec.ts
+++ b/web/gui2/src/main/webapp/tests/app/fw/util/fn.service.spec.ts
@@ -20,6 +20,7 @@
 import { FnService } from '../../../../app/fw/util/fn.service';
 import { ActivatedRoute, Params } from '@angular/router';
 import { of } from 'rxjs';
+import * as d3 from 'd3';
 
 class MockActivatedRoute extends ActivatedRoute {
     constructor(params: Params) {
@@ -32,22 +33,460 @@
  * ONOS GUI -- Util -- General Purpose Functions - Unit Tests
  */
 describe('FnService', () => {
-    let log: LogService;
     let ar: ActivatedRoute;
+    let fs: FnService;
+    let mockWindow: Window;
+    let logServiceSpy: jasmine.SpyObj<LogService>;
+
+    const someFunction = () => {};
+    const someArray = [1, 2, 3];
+    const someObject = { foo: 'bar'};
+    const someNumber = 42;
+    const someString = 'xyyzy';
+    const someDate = new Date();
+    const stringArray = ['foo', 'bar'];
 
     beforeEach(() => {
-        log = new ConsoleLoggerService();
+        const logSpy = jasmine.createSpyObj('LogService', ['debug', 'warn']);
         ar = new MockActivatedRoute({'debug': 'TestService'});
+        mockWindow = <any>{
+            innerWidth: 400,
+            innerHeight: 200,
+            navigator: {
+                userAgent: 'defaultUA'
+            }
+        };
+
 
         TestBed.configureTestingModule({
             providers: [FnService,
-                { provide: LogService, useValue: log },
+                { provide: LogService, useValue: logSpy },
                 { provide: ActivatedRoute, useValue: ar },
+                { provide: Window, useFactory: (() => mockWindow ) }
             ]
         });
+
+        fs = TestBed.get(FnService);
+        logServiceSpy = TestBed.get(LogService);
     });
 
-    it('should be created', inject([FnService], (service: FnService) => {
-        expect(service).toBeTruthy();
-    }));
+    it('should be created', () => {
+        expect(fs).toBeTruthy();
+    });
+
+    // === Tests for isF()
+    it('isF(): null for undefined', () => {
+        expect(fs.isF(undefined)).toBeNull();
+    });
+
+    it('isF(): null for null', () => {
+        expect(fs.isF(null)).toBeNull();
+    });
+    it('isF(): the reference for function', () => {
+        expect(fs.isF(someFunction)).toBe(someFunction);
+    });
+    it('isF(): null for string', () => {
+        expect(fs.isF(someString)).toBeNull();
+    });
+    it('isF(): null for number', () => {
+        expect(fs.isF(someNumber)).toBeNull();
+    });
+    it('isF(): null for Date', () => {
+        expect(fs.isF(someDate)).toBeNull();
+    });
+    it('isF(): null for array', () => {
+        expect(fs.isF(someArray)).toBeNull();
+    });
+    it('isF(): null for object', () => {
+        expect(fs.isF(someObject)).toBeNull();
+    });
+
+    // === Tests for isA()
+    it('isA(): null for undefined', () => {
+        expect(fs.isA(undefined)).toBeNull();
+    });
+    it('isA(): null for null', () => {
+        expect(fs.isA(null)).toBeNull();
+    });
+    it('isA(): null for function', () => {
+        expect(fs.isA(someFunction)).toBeNull();
+    });
+    it('isA(): null for string', () => {
+        expect(fs.isA(someString)).toBeNull();
+    });
+    it('isA(): null for number', () => {
+        expect(fs.isA(someNumber)).toBeNull();
+    });
+    it('isA(): null for Date', () => {
+        expect(fs.isA(someDate)).toBeNull();
+    });
+    it('isA(): the reference for array', () => {
+        expect(fs.isA(someArray)).toBe(someArray);
+    });
+    it('isA(): null for object', () => {
+        expect(fs.isA(someObject)).toBeNull();
+    });
+
+    // === Tests for isS()
+    it('isS(): null for undefined', () => {
+        expect(fs.isS(undefined)).toBeNull();
+    });
+    it('isS(): null for null', () => {
+        expect(fs.isS(null)).toBeNull();
+    });
+    it('isS(): null for function', () => {
+        expect(fs.isS(someFunction)).toBeNull();
+    });
+    it('isS(): the reference for string', () => {
+        expect(fs.isS(someString)).toBe(someString);
+    });
+    it('isS(): null for number', () => {
+        expect(fs.isS(someNumber)).toBeNull();
+    });
+    it('isS(): null for Date', () => {
+        expect(fs.isS(someDate)).toBeNull();
+    });
+    it('isS(): null for array', () => {
+        expect(fs.isS(someArray)).toBeNull();
+    });
+    it('isS(): null for object', () => {
+        expect(fs.isS(someObject)).toBeNull();
+    });
+
+    // === Tests for isO()
+    it('isO(): null for undefined', () => {
+        expect(fs.isO(undefined)).toBeNull();
+    });
+    it('isO(): null for null', () => {
+        expect(fs.isO(null)).toBeNull();
+    });
+    it('isO(): null for function', () => {
+        expect(fs.isO(someFunction)).toBeNull();
+    });
+    it('isO(): null for string', () => {
+        expect(fs.isO(someString)).toBeNull();
+    });
+    it('isO(): null for number', () => {
+        expect(fs.isO(someNumber)).toBeNull();
+    });
+    it('isO(): null for Date', () => {
+        expect(fs.isO(someDate)).toBeNull();
+    });
+    it('isO(): null for array', () => {
+        expect(fs.isO(someArray)).toBeNull();
+    });
+    it('isO(): the reference for object', () => {
+        expect(fs.isO(someObject)).toBe(someObject);
+    });
+
+
+    // === Tests for contains()
+    it('contains(): false for non-array', () => {
+        expect(fs.contains(null, 1)).toBeFalsy();
+    });
+    it('contains(): true for contained item', () => {
+        expect(fs.contains(someArray, 1)).toBeTruthy();
+        expect(fs.contains(stringArray, 'bar')).toBeTruthy();
+    });
+    it('contains(): false for non-contained item', () => {
+        expect(fs.contains(someArray, 109)).toBeFalsy();
+        expect(fs.contains(stringArray, 'zonko')).toBeFalsy();
+    });
+
+    // === Tests for areFunctions()
+    it('areFunctions(): true for empty-array', () => {
+        expect(fs.areFunctions({}, [])).toBeTruthy();
+    });
+    it('areFunctions(): true for some api', () => {
+        expect(fs.areFunctions({
+            a: () => {},
+            b: () => {}
+        }, ['b', 'a'])).toBeTruthy();
+    });
+    it('areFunctions(): false for some other api', () => {
+        expect(fs.areFunctions({
+            a: () => {},
+            b: 'not-a-function'
+        }, ['b', 'a'])).toBeFalsy();
+    });
+    it('areFunctions(): extraneous stuff NOT ignored', () => {
+        expect(fs.areFunctions({
+            a: () => {},
+            b: () => {},
+            c: 1,
+            d: 'foo'
+        }, ['a', 'b'])).toBeFalsy();
+    });
+    it('areFunctions(): extraneous stuff ignored (alternate fn)', () => {
+        expect(fs.areFunctionsNonStrict({
+            a: () => {},
+            b: () => {},
+            c: 1,
+            d: 'foo'
+        }, ['a', 'b'])).toBeTruthy();
+    });
+
+    // == use the now-tested areFunctions() on our own api:
+    it('should define api functions', () => {
+        expect(fs.areFunctions(fs, [
+            'isF', 'isA', 'isS', 'isO', 'contains',
+            'areFunctions', 'areFunctionsNonStrict', 'windowSize',
+            'isMobile', 'isChrome', 'isChromeHeadless', 'isSafari',
+            'isFirefox', 'parseDebugFlags',
+            'debugOn', 'debug', 'find', 'inArray', 'removeFromArray',
+            'isEmptyObject', 'cap', 'noPx', 'noPxStyle', 'endsWith',
+            'inEvilList', 'analyze', 'sanitize'
+//            'find', 'inArray', 'removeFromArray', 'isEmptyObject', 'sameObjProps', 'containsObj', 'cap',
+//            'eecode', 'noPx', 'noPxStyle', 'endsWith', 'addToTrie', 'removeFromTrie', 'trieLookup',
+//            'classNames', 'extend', 'sanitize'
+        ])).toBeTruthy();
+    });
+
+
+    // === Tests for windowSize()
+    it('windowSize(): adjust height', () => {
+        const dim = fs.windowSize(50);
+        expect(dim.width).toEqual(400);
+        expect(dim.height).toEqual(150);
+    });
+
+    it('windowSize(): adjust width', () => {
+        const dim = fs.windowSize(0, 50);
+        expect(dim.width).toEqual(350);
+        expect(dim.height).toEqual(200);
+    });
+
+    it('windowSize(): adjust width and height', () => {
+        const dim = fs.windowSize(101, 201);
+        expect(dim.width).toEqual(199);
+        expect(dim.height).toEqual(99);
+    });
+
+    // === Tests for isMobile()
+    const uaMap = {
+        chrome: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_2) ' +
+                'AppleWebKit/537.36 (KHTML, like Gecko) ' +
+                'Chrome/41.0.2272.89 Safari/537.36',
+
+        iPad: 'Mozilla/5.0 (iPad; CPU OS 7_0 like Mac OS X) ' +
+                'AppleWebKit/537.51.1 (KHTML, like Gecko) Version/7.0 ' +
+                'Mobile/11A465 Safari/9537.53',
+
+        iPhone: 'Mozilla/5.0 (iPhone; CPU iPhone OS 7_0 like Mac OS X) ' +
+                'AppleWebKit/537.51.1 (KHTML, like Gecko) Version/7.0 ' +
+                'Mobile/11A465 Safari/9537.53'
+    };
+
+    function setUa(key) {
+        const str = uaMap[key];
+        expect(str).toBeTruthy();
+        (<any>mockWindow.navigator).userAgent = str;
+    }
+
+    it('isMobile(): should be false for Chrome on Mac OS X', () => {
+        setUa('chrome');
+        expect(fs.isMobile()).toBe(false);
+    });
+    it('isMobile(): should be true for Safari on iPad', () => {
+        setUa('iPad');
+        expect(fs.isMobile()).toBe(true);
+    });
+    it('isMobile(): should be true for Safari on iPhone', () => {
+        setUa('iPhone');
+        expect(fs.isMobile()).toBe(true);
+    });
+
+    // === Tests for find()
+    const dataset = [
+        { id: 'foo', name: 'Furby'},
+        { id: 'bar', name: 'Barbi'},
+        { id: 'baz', name: 'Basil'},
+        { id: 'goo', name: 'Gabby'},
+        { id: 'zoo', name: 'Zevvv'}
+    ];
+
+    it('should not find ooo', () => {
+        expect(fs.find('ooo', dataset)).toEqual(-1);
+    });
+    it('should find foo', () => {
+        expect(fs.find('foo', dataset)).toEqual(0);
+    });
+    it('should find zoo', () => {
+        expect(fs.find('zoo', dataset)).toEqual(4);
+    });
+
+    it('should not find Simon', () => {
+        expect(fs.find('Simon', dataset, 'name')).toEqual(-1);
+    });
+    it('should find Furby', () => {
+        expect(fs.find('Furby', dataset, 'name')).toEqual(0);
+    });
+    it('should find Zevvv', () => {
+        expect(fs.find('Zevvv', dataset, 'name')).toEqual(4);
+    });
+
+
+    // === Tests for inArray()
+    const objRef = { x: 1, y: 2 };
+    const array = [1, 3.14, 'hey', objRef, 'there', true];
+    const array2 = ['b', 'a', 'd', 'a', 's', 's'];
+
+    it('should not find HOO', () => {
+        expect(fs.inArray('HOO', array)).toEqual(-1);
+    });
+    it('should find 1', () => {
+        expect(fs.inArray(1, array)).toEqual(0);
+    });
+    it('should find pi', () => {
+        expect(fs.inArray(3.14, array)).toEqual(1);
+    });
+    it('should find hey', () => {
+        expect(fs.inArray('hey', array)).toEqual(2);
+    });
+    it('should find the object', () => {
+        expect(fs.inArray(objRef, array)).toEqual(3);
+    });
+    it('should find there', () => {
+        expect(fs.inArray('there', array)).toEqual(4);
+    });
+    it('should find true', () => {
+        expect(fs.inArray(true, array)).toEqual(5);
+    });
+
+    it('should find the first occurrence A', () => {
+        expect(fs.inArray('a', array2)).toEqual(1);
+    });
+    it('should find the first occurrence S', () => {
+        expect(fs.inArray('s', array2)).toEqual(4);
+    });
+    it('should not find X', () => {
+        expect(fs.inArray('x', array2)).toEqual(-1);
+    });
+
+    // === Tests for removeFromArray()
+    it('should keep the array the same, for non-match', () => {
+        const array1 = [1, 2, 3];
+        expect(fs.removeFromArray(4, array1)).toBe(false);
+        expect(array1).toEqual([1, 2, 3]);
+    });
+    it('should remove a value', () => {
+        const array1a = [1, 2, 3];
+        expect(fs.removeFromArray(2, array1a)).toBe(true);
+        expect(array1a).toEqual([1, 3]);
+    });
+    it('should remove the first occurrence', () => {
+        const array1b = ['x', 'y', 'z', 'z', 'y'];
+        expect(fs.removeFromArray('y', array1b)).toBe(true);
+        expect(array1b).toEqual(['x', 'z', 'z', 'y']);
+        expect(fs.removeFromArray('x', array1b)).toBe(true);
+        expect(array1b).toEqual(['z', 'z', 'y']);
+    });
+
+    // === Tests for isEmptyObject()
+    it('should return true if an object is empty', () => {
+        expect(fs.isEmptyObject({})).toBe(true);
+    });
+    it('should return false if an object is not empty', () => {
+        expect(fs.isEmptyObject({foo: 'bar'})).toBe(false);
+    });
+
+    // === Tests for cap()
+    it('should ignore non-alpha', () => {
+        expect(fs.cap('123')).toEqual('123');
+    });
+    it('should capitalize first char', () => {
+        expect(fs.cap('Foo')).toEqual('Foo');
+        expect(fs.cap('foo')).toEqual('Foo');
+        expect(fs.cap('foo bar')).toEqual('Foo bar');
+        expect(fs.cap('FOO BAR')).toEqual('Foo bar');
+        expect(fs.cap('foo Bar')).toEqual('Foo bar');
+    });
+
+    // === Tests for noPx()
+    it('should return the value without px suffix', () => {
+        expect(fs.noPx('10px')).toBe(10);
+        expect(fs.noPx('500px')).toBe(500);
+        expect(fs.noPx('-80px')).toBe(-80);
+    });
+
+    // === Tests for noPxStyle()
+    it('should give a style\'s property without px suffix', () => {
+        const d3Elem = d3.select('body')
+            .append('div')
+            .attr('id', 'fooElem')
+            .style('width', '500px')
+            .style('height', '200px')
+            .style('font-size', '12px');
+        expect(fs.noPxStyle(d3Elem, 'width')).toBe(500);
+        expect(fs.noPxStyle(d3Elem, 'height')).toBe(200);
+        expect(fs.noPxStyle(d3Elem, 'font-size')).toBe(12);
+        d3.select('#fooElem').remove();
+    });
+
+    // === Tests for endsWith()
+    it('should return true if string ends with foo', () => {
+        expect(fs.endsWith('barfoo', 'foo')).toBe(true);
+    });
+
+    it('should return false if string doesnt end with foo', () => {
+        expect(fs.endsWith('barfood', 'foo')).toBe(false);
+    });
+
+    // === Tests for sanitize()
+    it('should return foo', () => {
+        expect(fs.sanitize('foo')).toEqual('foo');
+    });
+    it('should retain < b > tags', () => {
+        const str = 'foo <b>bar</b> baz';
+        expect(fs.sanitize(str)).toEqual(str);
+    });
+    it('should retain < i > tags', () => {
+        const str = 'foo <i>bar</i> baz';
+        expect(fs.sanitize(str)).toEqual(str);
+    });
+    it('should retain < p > tags', () => {
+        const str = 'foo <p>bar</p> baz';
+        expect(fs.sanitize(str)).toEqual(str);
+    });
+    it('should retain < em > tags', () => {
+        const str = 'foo <em>bar</em> baz';
+        expect(fs.sanitize(str)).toEqual(str);
+    });
+    it('should retain < strong > tags', () => {
+        const str = 'foo <strong>bar</strong> baz';
+        expect(fs.sanitize(str)).toEqual(str);
+    });
+
+    it('should reject < a > tags', () => {
+        expect(fs.sanitize('test <a href="hah">something</a> this'))
+            .toEqual('test something this');
+    });
+
+    it('should log a warning for < script > tags', () => {
+        expect(fs.sanitize('<script>alert("foo");</script>'))
+            .toEqual('');
+        expect(logServiceSpy.warn).toHaveBeenCalledWith(
+            'Unsanitary HTML input -- <script> detected!'
+        );
+    });
+    it('should log a warning for < style > tags', () => {
+        expect(fs.sanitize('<style> h1 {color:red;} </style>'))
+            .toEqual('');
+        expect(logServiceSpy.warn).toHaveBeenCalledWith(
+            'Unsanitary HTML input -- <style> detected!'
+        );
+    });
+
+    it('should log a warning for < iframe > tags', () => {
+        expect(fs.sanitize('Foo<iframe><body><h1>fake</h1></body></iframe>Bar'))
+            .toEqual('FooBar');
+        expect(logServiceSpy.warn).toHaveBeenCalledWith(
+            'Unsanitary HTML input -- <iframe> detected!'
+        );
+    });
+
+    it('should completely strip < script >, remove < a >, retain < i >', () => {
+        expect(fs.sanitize('Hey <i>this</i> is <script>alert("foo");</script> <a href="meh">cool</a>'))
+            .toEqual('Hey <i>this</i> is  cool');
+    });
 });
diff --git a/web/gui2/src/main/webapp/tests/app/fw/util/lion.service.spec.ts b/web/gui2/src/main/webapp/tests/app/fw/util/lion.service.spec.ts
index d471005..6535f07 100644
--- a/web/gui2/src/main/webapp/tests/app/fw/util/lion.service.spec.ts
+++ b/web/gui2/src/main/webapp/tests/app/fw/util/lion.service.spec.ts
@@ -15,27 +15,64 @@
  *
  */
 import { TestBed, inject } from '@angular/core/testing';
+import { of } from 'rxjs';
 
 import { LogService } from '../../../../app/log.service';
 import { ConsoleLoggerService } from '../../../../app/consolelogger.service';
+import { ActivatedRoute, Params } from '@angular/router';
+import { FnService } from '../../../../app/fw/util/fn.service';
+import { GlyphService } from '../../../../app/fw/svg/glyph.service';
 import { LionService } from '../../../../app/fw/util/lion.service';
-import { WebSocketService } from '../../../../app/fw/remote/websocket.service';
+import { UrlFnService } from '../../../../app/fw/remote/urlfn.service';
+import { WSock } from '../../../../app/fw/remote/wsock.service';
+import { WebSocketService, WsOptions } from '../../../../app/fw/remote/websocket.service';
 
-class MockWebSocketService {}
+class MockActivatedRoute extends ActivatedRoute {
+    constructor(params: Params) {
+        super();
+        this.queryParams = of(params);
+    }
+}
+
+class MockWSock {}
+
+class MockGlyphService {}
+
+class MockUrlFnService {}
 
 /**
  * ONOS GUI -- Lion -- Localization Utilities - Unit Tests
  */
 describe('LionService', () => {
     let log: LogService;
+    let fs: FnService;
+    let ar: MockActivatedRoute;
+    let windowMock: Window;
 
     beforeEach(() => {
         log = new ConsoleLoggerService();
+        ar = new MockActivatedRoute({'debug': 'TestService'});
+        windowMock = <any>{
+            location: <any> {
+                hostname: '',
+                host: '',
+                port: '',
+                protocol: '',
+                search: { debug: 'true'},
+                href: ''
+            }
+        };
+        fs = new FnService(ar, log, windowMock);
 
         TestBed.configureTestingModule({
             providers: [LionService,
+                { provide: FnService, useValue: fs },
+                { provide: GlyphService, useClass: MockGlyphService },
                 { provide: LogService, useValue: log },
-                { provide: WebSocketService, useClass: MockWebSocketService },
+                { provide: UrlFnService, useClass: MockUrlFnService },
+                { provide: WSock, useClass: MockWSock },
+                { provide: WebSocketService, useClass: WebSocketService },
+                { provide: Window, useFactory: (() => windowMock ) },
             ]
         });
     });
diff --git a/web/gui2/src/main/webapp/tests/app/onos.component.spec.ts b/web/gui2/src/main/webapp/tests/app/onos.component.spec.ts
index 2831d97..7a15504 100644
--- a/web/gui2/src/main/webapp/tests/app/onos.component.spec.ts
+++ b/web/gui2/src/main/webapp/tests/app/onos.component.spec.ts
@@ -14,15 +14,21 @@
  * limitations under the License.
  */
 import { TestBed, async } from '@angular/core/testing';
-import { RouterModule, RouterOutlet, ChildrenOutletContexts } from '@angular/router';
+import { RouterModule, RouterOutlet, ChildrenOutletContexts, ActivatedRoute, Params } from '@angular/router';
+import { of } from 'rxjs';
+
 import { LogService } from '../../app/log.service';
 import { ConsoleLoggerService } from '../../app/consolelogger.service';
+
 import { IconComponent } from '../../app/fw/svg/icon/icon.component';
 import { MastComponent } from '../../app/fw/mast/mast/mast.component';
 import { NavComponent } from '../../app/fw/nav/nav/nav.component';
 import { OnosComponent } from '../../app/onos.component';
+import { VeilComponent } from '../../app/fw/layer/veil/veil.component';
+
 import { DialogService } from '../../app/fw/layer/dialog.service';
 import { EeService } from '../../app/fw/util/ee.service';
+import { FnService } from '../../app/fw/util/fn.service';
 import { GlyphService } from '../../app/fw/svg/glyph.service';
 import { IconService } from '../../app/fw/svg/icon.service';
 import { KeyService } from '../../app/fw/util/key.service';
@@ -31,10 +37,17 @@
 import { OnosService } from '../../app/onos.service';
 import { PanelService } from '../../app/fw/layer/panel.service';
 import { QuickHelpService } from '../../app/fw/layer/quickhelp.service';
+import { SvgUtilService } from '../../app/fw/svg/svgutil.service';
 import { ThemeService } from '../../app/fw/util/theme.service';
 import { SpriteService } from '../../app/fw/svg/sprite.service';
-import { VeilService } from '../../app/fw/layer/veil.service';
-import { WebSocketService } from '../../app/fw/remote/websocket.service';
+import { WebSocketService, WsOptions } from '../../app/fw/remote/websocket.service';
+
+class MockActivatedRoute extends ActivatedRoute {
+    constructor(params: Params) {
+        super();
+        this.queryParams = of(params);
+    }
+}
 
 class MockDialogService {}
 
@@ -46,8 +59,6 @@
 
 class MockKeyService {}
 
-class MockLionService {}
-
 class MockNavService {}
 
 class MockOnosService {}
@@ -60,18 +71,34 @@
 
 class MockThemeService {}
 
-class MockVeilService {}
-
-class MockWebSocketService {}
+class MockVeilComponent {}
 
 /**
  * ONOS GUI -- Onos Component - Unit Tests
  */
 describe('OnosComponent', () => {
     let log: LogService;
+    let fs: FnService;
+    let ar: MockActivatedRoute;
+    let windowMock: Window;
 
     beforeEach(async(() => {
         log = new ConsoleLoggerService();
+        ar = new MockActivatedRoute({'debug': 'TestService'});
+
+        windowMock = <any>{
+            location: <any> {
+                hostname: '',
+                host: '',
+                port: '',
+                protocol: '',
+                search: { debug: 'true'},
+                href: ''
+            },
+            innerHeight: 240,
+            innerWidth: 320
+        };
+        fs = new FnService(ar, log, windowMock);
 
         TestBed.configureTestingModule({
             declarations: [
@@ -79,16 +106,17 @@
                 MastComponent,
                 NavComponent,
                 OnosComponent,
+                VeilComponent,
                 RouterOutlet
             ],
             providers: [
                 { provide: ChildrenOutletContexts, useClass: ChildrenOutletContexts },
                 { provide: DialogService, useClass: MockDialogService },
                 { provide: EeService, useClass: MockEeService },
+                { provide: FnService, useValue: fs },
                 { provide: GlyphService, useClass: MockGlyphService },
                 { provide: IconService, useClass: MockIconService },
                 { provide: KeyService, useClass: MockKeyService },
-                { provide: LionService, useClass: MockLionService },
                 { provide: LogService, useValue: log },
                 { provide: NavService, useClass: MockNavService },
                 { provide: OnosService, useClass: MockOnosService },
@@ -96,8 +124,7 @@
                 { provide: PanelService, useClass: MockPanelService },
                 { provide: SpriteService, useClass: MockSpriteService },
                 { provide: ThemeService, useClass: MockThemeService },
-                { provide: VeilService, useClass: MockVeilService },
-                { provide: WebSocketService, useClass: MockWebSocketService },
+                { provide: Window, useFactory: (() => windowMock ) },
             ]
         }).compileComponents();
     }));
diff --git a/web/gui2/src/main/webapp/tests/app/view/device/device.component.spec.ts b/web/gui2/src/main/webapp/tests/app/view/device/device.component.spec.ts
index edfaed4..960d241 100644
--- a/web/gui2/src/main/webapp/tests/app/view/device/device.component.spec.ts
+++ b/web/gui2/src/main/webapp/tests/app/view/device/device.component.spec.ts
@@ -18,24 +18,36 @@
 import { LogService } from '../../../../app/log.service';
 import { ConsoleLoggerService } from '../../../../app/consolelogger.service';
 import { DeviceComponent } from '../../../../app/view/device/device.component';
+
 import { DetailsPanelService } from '../../../../app/fw/layer/detailspanel.service';
-import { FnService } from '../../../../app/fw/util/fn.service';
+import { FnService, WindowSize } from '../../../../app/fw/util/fn.service';
 import { IconService } from '../../../../app/fw/svg/icon.service';
+import { GlyphService } from '../../../../app/fw/svg/glyph.service';
 import { KeyService } from '../../../../app/fw/util/key.service';
 import { LoadingService } from '../../../../app/fw/layer/loading.service';
 import { NavService } from '../../../../app/fw/nav/nav.service';
 import { MastService } from '../../../../app/fw/mast/mast.service';
 import { PanelService } from '../../../../app/fw/layer/panel.service';
+import { SvgUtilService } from '../../../../app/fw/svg/svgutil.service';
 import { TableBuilderService } from '../../../../app/fw/widget/tablebuilder.service';
 import { TableDetailService } from '../../../../app/fw/widget/tabledetail.service';
 import { WebSocketService } from '../../../../app/fw/remote/websocket.service';
 
 class MockDetailsPanelService {}
 
-class MockFnService {}
+class MockFnService {
+    windowSize(offH: number = 0, offW: number = 0): WindowSize {
+        return {
+            height: 123,
+            width: 456
+        };
+    }
+}
 
 class MockIconService {}
 
+class MockGlyphService {}
+
 class MockKeyService {}
 
 class MockLoadingService {
@@ -74,6 +86,7 @@
                 { provide: DetailsPanelService, useClass: MockDetailsPanelService },
                 { provide: FnService, useClass: MockFnService },
                 { provide: IconService, useClass: MockIconService },
+                { provide: GlyphService, useClass: MockGlyphService },
                 { provide: KeyService, useClass: MockKeyService },
                 { provide: LoadingService, useClass: MockLoadingService },
                 { provide: MastService, useClass: MockMastService },
@@ -91,7 +104,7 @@
 
     beforeEach(() => {
         fixture = TestBed.createComponent(DeviceComponent);
-            component = fixture.componentInstance;
+        component = fixture.componentInstance;
         fixture.detectChanges();
     });