Initial (v.rough) draft of ONOS UI.
Finally got something working, and need to check it in.
diff --git a/web/gui/src/main/webapp/base.css b/web/gui/src/main/webapp/base.css
new file mode 100644
index 0000000..51f16fe
--- /dev/null
+++ b/web/gui/src/main/webapp/base.css
@@ -0,0 +1,15 @@
+/*
+ Base CSS file
+
+ @author Simon Hunt
+ */
+
+html {
+    font-family: sans-serif;
+    -webkit-text-size-adjust: 100%;
+    -ms-text-size-adjust: 100%;
+}
+
+body {
+    margin: 0;
+}
diff --git a/web/gui/src/main/webapp/index.html b/web/gui/src/main/webapp/index.html
index d68a706..3b0d290 100644
--- a/web/gui/src/main/webapp/index.html
+++ b/web/gui/src/main/webapp/index.html
@@ -1,13 +1,54 @@
 <!DOCTYPE html>
+<!--
+  ONOS UI - single page web app
+
+  @author Simon Hunt
+  -->
 <html>
 <head>
     <title>ONOS GUI</title>
 
     <script src="libs/d3.min.js"></script>
     <script src="libs/jquery-2.1.1.min.js"></script>
+
+    <link rel="stylesheet" href="base.css">
+    <link rel="stylesheet" href="onos.css">
+
+    <script src="onosui.js"></script>
+
 </head>
 <body>
-    <h1>ONOS GUI</h1>
-    Sort of...
+    <div id="frame">
+        <div id="mast">
+            <span class="title">
+                ONOS Web UI
+            </span>
+            <span class="right">
+                <span class="radio">[one]</span>
+                <span class="radio">[two]</span>
+                <span class="radio">[three]</span>
+            </span>
+        </div>
+        <div id="view"></div>
+    </div>
+
+    // Initialize the UI...
+    <script type="text/javascript">
+        var ONOS = $.onos({note: "config, if needed"});
+    </script>
+
+    // include module files
+    // + mast.js
+    // + nav.js
+    // + .... application views
+
+    // for now, we are just bootstrapping the network visualization
+    <script src="network.js" type="text/javascript"></script>
+
+    // finally, build the UI
+    <script type="text/javascript">
+        $(ONOS.buildUi);
+    </script>
+
 </body>
-</html>
\ No newline at end of file
+</html>
diff --git a/web/gui/src/main/webapp/module-template.js b/web/gui/src/main/webapp/module-template.js
new file mode 100644
index 0000000..da049fe
--- /dev/null
+++ b/web/gui/src/main/webapp/module-template.js
@@ -0,0 +1,20 @@
+/*
+ Module template file.
+
+ @author Simon Hunt
+ */
+
+(function (onos) {
+    'use strict';
+
+    var api = onos.api;
+
+    // == define your functions here.....
+
+
+    // == register views here, with links to lifecycle callbacks
+
+//    api.addView('view-id', {/* callbacks */});
+
+
+}(ONOS));
diff --git a/web/gui/src/main/webapp/network.js b/web/gui/src/main/webapp/network.js
new file mode 100644
index 0000000..81105dc
--- /dev/null
+++ b/web/gui/src/main/webapp/network.js
@@ -0,0 +1,374 @@
+/*
+ ONOS network topology viewer - PoC version 1.0
+
+ @author Simon Hunt
+ */
+
+(function (onos) {
+    'use strict';
+
+    var api = onos.api;
+
+    var config = {
+            jsonUrl: 'network.json',
+            mastHeight: 32,
+            force: {
+                linkDistance: 150,
+                linkStrength: 0.9,
+                charge: -400,
+                ticksWithoutCollisions: 50,
+                marginLR: 20,
+                marginTB: 20,
+                translate: function() {
+                    return 'translate(' +
+                        config.force.marginLR + ',' +
+                        config.force.marginTB + ')';
+                }
+            },
+            labels: {
+                padLR: 3,
+                padTB: 2,
+                marginLR: 3,
+                marginTB: 2
+            },
+            constraints: {
+                ypos: {
+                    pkt: 0.3,
+                    opt: 0.7
+                }
+            }
+        },
+        view = {},
+        network = {},
+        selected = {},
+        highlighted = null;
+
+
+    function loadNetworkView() {
+        // Hey, here I am, calling something on the ONOS api:
+        api.printTime();
+
+        resize();
+
+        d3.json(config.jsonUrl, function (err, data) {
+            if (err) {
+                alert('Oops! Error reading JSON...\n\n' +
+                    'URL: ' + jsonUrl + '\n\n' +
+                    'Error: ' + err.message);
+                return;
+            }
+            console.log("here is the JSON data...");
+            console.log(data);
+
+            network.data = data;
+            drawNetwork();
+        });
+
+        $(document).on('click', '.select-object', function() {
+            // when any object of class "select-object" is clicked...
+            // TODO: get a reference to the object via lookup...
+            var obj = network.lookup[$(this).data('id')];
+            if (obj) {
+                selectObject(obj);
+            }
+            // stop propagation of event (I think) ...
+            return false;
+        });
+
+        $(window).on('resize', resize);
+    }
+
+
+    // ========================================================
+
+    function drawNetwork() {
+        $('#view').empty();
+
+        prepareNodesAndLinks();
+        createLayout();
+        console.log("\n\nHere is the augmented network object...");
+        console.warn(network);
+    }
+
+    function prepareNodesAndLinks() {
+        network.lookup = {};
+        network.nodes = [];
+        network.links = [];
+
+        var nw = network.forceWidth,
+            nh = network.forceHeight;
+
+        network.data.nodes.forEach(function(n) {
+            var ypc = yPosConstraintForNode(n),
+                ix = Math.random() * 0.8 * nw + 0.1 * nw,
+                iy = ypc * nh,
+                node = {
+                    id: n.id,
+                    type: n.type,
+                    status: n.status,
+                    x: ix,
+                    y: iy,
+                    constraint: {
+                        weight: 0.7,
+                        y: iy
+                    }
+                };
+            network.lookup[n.id] = node;
+            network.nodes.push(node);
+        });
+
+        function yPosConstraintForNode(n) {
+            return config.constraints.ypos[n.type] || 0.5;
+        }
+
+
+        network.data.links.forEach(function(n) {
+            var src = network.lookup[n.src],
+                dst = network.lookup[n.dst],
+                id = src.id + "~" + dst.id;
+
+            var link = {
+                id: id,
+                source: src,
+                target: dst,
+                strength: config.force.linkStrength
+            };
+            network.links.push(link);
+        });
+    }
+
+    function createLayout() {
+
+        network.force = d3.layout.force()
+            .nodes(network.nodes)
+            .links(network.links)
+            .linkStrength(function(d) { return d.strength; })
+            .size([network.forceWidth, network.forceHeight])
+            .linkDistance(config.force.linkDistance)
+            .charge(config.force.charge)
+            .on('tick', tick);
+
+        network.svg = d3.select('#view').append('svg')
+            .attr('width', view.width)
+            .attr('height', view.height)
+            .append('g')
+            .attr('transform', config.force.translate());
+
+        // TODO: svg.append('defs')
+        // TODO: glow/blur stuff
+        // TODO: legend (and auto adjust on scroll)
+
+        network.link = network.svg.append('g').selectAll('.link')
+            .data(network.force.links(), function(d) {return d.id})
+            .enter().append('line')
+            .attr('class', 'link');
+
+        // TODO: drag behavior
+        // TODO: closest node deselect
+
+        // TODO: add drag, mouseover, mouseout behaviors
+        network.node = network.svg.selectAll('.node')
+            .data(network.force.nodes(), function(d) {return d.id})
+            .enter().append('g')
+            .attr('class', 'node')
+            .attr('transform', function(d) {
+                return translate(d.x, d.y);
+            })
+        //        .call(network.drag)
+            .on('mouseover', function(d) {})
+            .on('mouseout', function(d) {});
+
+        // TODO: augment stroke and fill functions
+        network.nodeRect = network.node.append('rect')
+            // TODO: css for node rects
+            .attr('rx', 5)
+            .attr('ry', 5)
+            .attr('stroke', function(d) { return '#000'})
+            .attr('fill', function(d) { return '#ddf'})
+            .attr('width', 60)
+            .attr('height', 24);
+
+        network.node.each(function(d) {
+            var node = d3.select(this),
+                rect = node.select('rect');
+            var text = node.append('text')
+                .text(d.id)
+                .attr('dx', '1em')
+                .attr('dy', '2.1em');
+        });
+
+        // this function is scheduled to happen soon after the given thread ends
+        setTimeout(function() {
+            network.node.each(function(d) {
+                // for every node, recompute size, padding, etc. so text fits
+                var node = d3.select(this),
+                    text = node.selectAll('text'),
+                    bounds = {},
+                    first = true;
+
+                // NOTE: probably unnecessary code if we only have one line.
+            });
+
+            network.numTicks = 0;
+            network.preventCollisions = false;
+            network.force.start();
+            for (var i = 0; i < config.ticksWithoutCollisions; i++) {
+                network.force.tick();
+            }
+            network.preventCollisions = true;
+            $('#view').css('visibility', 'visible');
+        });
+
+    }
+
+    function translate(x, y) {
+        return 'translate(' + x + ',' + y + ')';
+    }
+
+
+    function tick(e) {
+        network.numTicks++;
+
+        // adjust the y-coord of each node, based on y-pos constraints
+//        network.nodes.forEach(function (n) {
+//            var z = e.alpha * n.constraint.weight;
+//            if (!isNaN(n.constraint.y)) {
+//                n.y = (n.constraint.y * z + n.y * (1 - z));
+//            }
+//        });
+
+        network.link
+            .attr('x1', function(d) {
+                return d.source.x;
+            })
+            .attr('y1', function(d) {
+                return d.source.y;
+            })
+            .attr('x2', function(d) {
+                return d.target.x;
+            })
+            .attr('y2', function(d) {
+                return d.target.y;
+            });
+
+        network.node
+            .attr('transform', function(d) {
+                return translate(d.x, d.y);
+            });
+
+    }
+
+    //    $('#docs-close').on('click', function() {
+    //        deselectObject();
+    //        return false;
+    //    });
+
+    //    $(document).on('click', '.select-object', function() {
+    //        var obj = graph.data[$(this).data('name')];
+    //        if (obj) {
+    //            selectObject(obj);
+    //        }
+    //        return false;
+    //    });
+
+    function selectObject(obj, el) {
+        var node;
+        if (el) {
+            node = d3.select(el);
+        } else {
+            network.node.each(function(d) {
+                if (d == obj) {
+                    node = d3.select(el = this);
+                }
+            });
+        }
+        if (!node) return;
+
+        if (node.classed('selected')) {
+            deselectObject();
+            return;
+        }
+        deselectObject(false);
+
+        selected = {
+            obj : obj,
+            el  : el
+        };
+
+        highlightObject(obj);
+
+        node.classed('selected', true);
+
+        // TODO animate incoming info pane
+        // resize(true);
+        // TODO: check bounds of selected node and scroll into view if needed
+    }
+
+    function deselectObject(doResize) {
+        // Review: logic of 'resize(...)' function.
+        if (doResize || typeof doResize == 'undefined') {
+            resize(false);
+        }
+        // deselect all nodes in the network...
+        network.node.classed('selected', false);
+        selected = {};
+        highlightObject(null);
+    }
+
+    function highlightObject(obj) {
+        if (obj) {
+            if (obj != highlighted) {
+                // TODO set or clear "inactive" class on nodes, based on criteria
+                network.node.classed('inactive', function(d) {
+                    //                return (obj !== d &&
+                    //                    d.relation(obj.id));
+                    return (obj !== d);
+                });
+                // TODO: same with links
+                network.link.classed('inactive', function(d) {
+                    return (obj !== d.source && obj !== d.target);
+                });
+            }
+            highlighted = obj;
+        } else {
+            if (highlighted) {
+                // clear the inactive flag (no longer suppressed visually)
+                network.node.classed('inactive', false);
+                network.link.classed('inactive', false);
+            }
+            highlighted = null;
+
+        }
+    }
+
+    function resize(showDetails) {
+        console.log("resize() called...");
+
+        var $details = $('#details');
+
+        if (typeof showDetails == 'boolean') {
+            var showingDetails = showDetails;
+            // TODO: invoke $details.show() or $details.hide()...
+            //        $details[showingDetails ? 'show' : 'hide']();
+        }
+
+        view.height = window.innerHeight - config.mastHeight;
+        view.width = window.innerWidth;
+        $('#view')
+            .css('height', view.height + 'px')
+            .css('width', view.width + 'px');
+
+        network.forceWidth = view.width - config.force.marginLR;
+        network.forceHeight = view.height - config.force.marginTB;
+    }
+
+    // ======================================================================
+    // register with the UI framework
+
+    api.addView('network', {
+        load: loadNetworkView
+    });
+
+
+}(ONOS));
+
diff --git a/web/gui/src/main/webapp/network.json b/web/gui/src/main/webapp/network.json
new file mode 100644
index 0000000..74a276a
--- /dev/null
+++ b/web/gui/src/main/webapp/network.json
@@ -0,0 +1,56 @@
+{
+    "id": "network-v1",
+    "meta": {
+        "__comment_1__": "This is sample data for developing the ONOS UI",
+        "foo": "bar",
+        "zoo": "goo"
+    },
+    "nodes": [
+        {
+            "id": "switch-1",
+            "type": "opt",
+            "status": "good"
+        },
+        {
+            "id": "switch-2",
+            "type": "opt",
+            "status": "good"
+        },
+        {
+            "id": "switch-3",
+            "type": "opt",
+            "status": "good"
+        },
+        {
+            "id": "switch-4",
+            "type": "opt",
+            "status": "good"
+        },
+        {
+            "id": "switch-11",
+            "type": "pkt",
+            "status": "good"
+        },
+        {
+            "id": "switch-12",
+            "type": "pkt",
+            "status": "good"
+        },
+        {
+            "id": "switch-13",
+            "type": "pkt",
+            "status": "good"
+        }
+    ],
+    "links": [
+        { "src": "switch-1", "dst": "switch-2" },
+        { "src": "switch-1", "dst": "switch-3" },
+        { "src": "switch-1", "dst": "switch-4" },
+        { "src": "switch-2", "dst": "switch-3" },
+        { "src": "switch-2", "dst": "switch-4" },
+        { "src": "switch-3", "dst": "switch-4" },
+        { "src": "switch-13", "dst": "switch-3" },
+        { "src": "switch-12", "dst": "switch-2" },
+        { "src": "switch-11", "dst": "switch-1" }
+    ]
+}
diff --git a/web/gui/src/main/webapp/onos.css b/web/gui/src/main/webapp/onos.css
new file mode 100644
index 0000000..328e109
--- /dev/null
+++ b/web/gui/src/main/webapp/onos.css
@@ -0,0 +1,124 @@
+/*
+ ONOS CSS file
+
+ @author Simon Hunt
+ */
+
+body, html {
+    height: 100%;
+}
+
+/*
+ * Classes
+ */
+
+span.title {
+    color: red;
+    font-size: 16pt;
+    font-style: italic;
+}
+
+span.radio {
+    color: darkslateblue;
+}
+
+span.right {
+    float: right;
+}
+
+/*
+ * === DEBUGGING ======
+ */
+svg {
+    border: 1px dashed red;
+}
+
+
+/*
+ * Network Graph elements ======================================
+ */
+
+.link {
+    fill: none;
+    stroke: #666;
+    stroke-width: 1.5px;
+    opacity: .7;
+    /*marker-end: url(#end);*/
+
+    transition: opacity 250ms;
+    -webkit-transition: opacity 250ms;
+    -moz-transition: opacity 250ms;
+}
+
+marker#end {
+    fill: #666;
+    stroke: #666;
+    stroke-width: 1.5px;
+}
+
+.node rect {
+    stroke-width: 1.5px;
+
+    transition: opacity 250ms;
+    -webkit-transition: opacity 250ms;
+    -moz-transition: opacity 250ms;
+}
+
+.node text {
+    fill: #000;
+    font: 10px sans-serif;
+    pointer-events: none;
+}
+
+.node.selected rect {
+    filter: url(#blue-glow);
+}
+
+.link.inactive,
+.node.inactive rect,
+.node.inactive text {
+    opacity: .2;
+}
+
+.node.inactive.selected rect,
+.node.inactive.selected text {
+    opacity: .6;
+}
+
+.legend {
+    position: fixed;
+}
+
+.legend .category rect {
+    stroke-width: 1px;
+}
+
+.legend .category text {
+    fill: #000;
+    font: 10px sans-serif;
+    pointer-events: none;
+}
+
+/*
+ * =============================================================
+ */
+
+/*
+ * Specific structural elements
+ */
+
+#frame {
+    width: 100%;
+    height: 100%;
+    background-color: #ffd;
+}
+
+#mast {
+    height: 32px;
+    background-color: #dda;
+    vertical-align: baseline;
+}
+
+#main {
+    background-color: #99b;
+}
diff --git a/web/gui/src/main/webapp/onosui.js b/web/gui/src/main/webapp/onosui.js
new file mode 100644
index 0000000..3b62548
--- /dev/null
+++ b/web/gui/src/main/webapp/onosui.js
@@ -0,0 +1,79 @@
+/*
+ ONOS UI Framework.
+
+ @author Simon Hunt
+ */
+
+(function ($) {
+    'use strict';
+    var tsI = new Date().getTime(),         // initialize time stamp
+        tsB;                                // build time stamp
+
+    // attach our main function to the jQuery object
+    $.onos = function (options) {
+        // private namespaces
+        var publicApi;             // public api
+
+        // internal state
+        var views = {},
+            currentView = null,
+            built = false;
+
+        // DOM elements etc.
+        var $mast;
+
+
+        // various functions..................
+
+        // throw an error
+        function throwError(msg) {
+            // todo: maybe add tracing later
+            throw new Error(msg);
+        }
+
+        // define all the public api functions...
+        publicApi = {
+            printTime: function () {
+                console.log("the time is " + new Date());
+            },
+
+            addView: function (vid, cb) {
+                views[vid] = {
+                    vid: vid,
+                    cb: cb
+                };
+                // TODO: proper registration of views
+                // for now, make the one (and only) view current..
+                currentView = views[vid];
+            }
+        };
+
+        // function to be called from index.html to build the ONOS UI
+        function buildOnosUi() {
+            tsB = new Date().getTime();
+            tsI = tsB - tsI; // initialization duration
+
+            console.log('ONOS UI initialized in ' + tsI + 'ms');
+
+            if (built) {
+                throwError("ONOS UI already built!");
+            }
+            built = true;
+
+            // TODO: invoke hash navigation
+            // --- report build errors ---
+
+            // for now, invoke the one and only load function:
+
+            currentView.cb.load();
+        }
+
+
+        // export the api and build-UI function
+        return {
+            api: publicApi,
+            buildUi: buildOnosUi
+        };
+    };
+
+}(jQuery));
\ No newline at end of file